PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 All 32 releases
xspeed / includes / js / delay-bootstrap.js

delay-bootstrap.js in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.6, at includes/js/delay-bootstrap.js

678 lines 29.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Delay bootstrap: the inline script Delay JS prints once, on wp_footer.
3 *
4 * It waits for the visitor's first interaction (or the failsafe timeout),
5 * then runs the replay: every delayed script is put back, in page order,
6 * and each one hears DOMContentLoaded, readystatechange and load once, as
7 * if it had run while the page loaded (#494).
8 *
9 * This is the readable source. `npm run build` minifies it into
10 * assets/delay-bootstrap.min.js with scripts/minify-delay-bootstrap.mjs,
11 * which only minifies: it adds no wrapper, helper or strict-mode line.
12 * Minify_Filters::print_delay_bootstrap() inlines the built copy and puts
13 * the timeout in place of XSPEED_DELAY_TIMEOUT at the very end. Edit this
14 * file, rebuild, and commit both; a unit test fails when the built copy is
15 * stale. Behaviour is pinned in a real browser by
16 * tests/e2e/85-delay-js-replay-harness.spec.ts, and the choice to rename
17 * listeners rather than fire the real events again is ADR 0001.
18 *
19 * Why the replay renames listeners. It runs after the page has loaded, so
20 * DOMContentLoaded and load have already fired, and a delayed script that
21 * sets itself up in one of those listeners is never called. Dispatching the
22 * real events again would run every eager script's handlers a second time.
23 * Instead, while the replay runs, addEventListener/removeEventListener on
24 * document and window file the three lifecycle events under private xs-*
25 * names. Only code running during the replay registers there, so
26 * dispatching the private names reaches exactly the replayed scripts. The
27 * dispatched event reports the real `type`: shared handlers and jQuery's
28 * dispatcher look handlers up by it.
29 *
30 * Properties this sets on script elements (plain properties, so the
31 * minifier leaves their names alone):
32 * - _xs: one of our clones of a delayed script;
33 * - _xe: on the page, and not delayed, when the replay started;
34 * - _xf: written into the page by our document.write redirect;
35 * - _xw: where the next write from that script goes.
36 */
37 (function (timeout) {
38 var TRIGGERS = ['mousemove', 'keydown', 'touchstart', 'scroll', 'wheel'];
39 var PRIVATE = {
40 DOMContentLoaded: 'xs-DOMContentLoaded',
41 load: 'xs-load',
42 readystatechange: 'xs-readystatechange',
43 };
44
45 // The real readyState, read past any getter defined on document itself
46 // (ours during the replay, or one the page defined).
47 var nativeReadyState = Object.getOwnPropertyDescriptor(Document.prototype, 'readyState');
48 function realReadyState() {
49 return nativeReadyState ? nativeReadyState.get.call(document) : document.readyState;
50 }
51
52 // readyState cannot say whether DOMContentLoaded has fired: it turns
53 // 'interactive' BEFORE the defer scripts and modules run. This runs
54 // inline in the footer, before DOMContentLoaded, so a listener records
55 // the real event; Navigation Timing covers a bootstrap that ran later.
56 // The same for load.
57 var started = false;
58 var dclFired = realReadyState() === 'complete';
59 var loadFired = dclFired;
60 var onRealDcl;
61 document.addEventListener('DOMContentLoaded', function () {
62 dclFired = true;
63 if (onRealDcl) onRealDcl();
64 });
65 window.addEventListener('load', function () {
66 loadFired = true;
67 });
68
69 // Dispatch the private copy of a lifecycle event under its real type.
70 // The dispatched load reports document as its target, as the real one
71 // does.
72 function firePrivate(target, type, bubbles) {
73 var event = new Event(PRIVATE[type], { bubbles: !!bubbles });
74 Object.defineProperty(event, 'type', { value: type });
75 if (type === 'load') Object.defineProperty(event, 'target', { value: document });
76 target.dispatchEvent(event);
77 }
78
79 function callHandler(handler, target, type) {
80 if (typeof handler !== 'function') return;
81 try {
82 handler.call(target, new Event(type));
83 } catch (e) {}
84 }
85
86 function typeOf(script) {
87 return (script.getAttribute('type') || '').trim().toLowerCase();
88 }
89
90 // Whether the browser will execute a script of this type. One it will
91 // not run (text/plain, nomodule, text/babel) fires neither load nor
92 // error, so counting it would hold the replay open.
93 function willRun(script, type) {
94 return !script.noModule && /^$|^module$|^(text|application)\/(x-)?(java|ecma|j|live)script$/.test(type);
95 }
96
97 function start() {
98 if (started) return;
99 started = true;
100 TRIGGERS.forEach(function (name) {
101 window.removeEventListener(name, start, { passive: true, capture: true });
102 });
103 var nav = window.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0];
104 if (nav && nav.domContentLoadedEventStart > 0) dclFired = true;
105
106 // pageLoaded: the replay starts after load, the usual case ("late").
107 // phase: 0 until the synthetic DOMContentLoaded, then 1 until load.
108 // pending: what the current phase still waits for; it starts at 1 for
109 // the loop below. heldInlines: inline scripts still waiting behind an
110 // external. modules: inline module index -> 1 inserted, 2 running
111 // between its markers, 0 done.
112 var pageLoaded = loadFired;
113 var live = 1;
114 var wrapped = [];
115 var pending = 1;
116 var deadline;
117 var phase = 0;
118 var heldInlines = 0;
119 var modules = {};
120
121 // When the replay starts before the real DOMContentLoaded, wait for it
122 // before sending our own copy, so a script that registered after it
123 // still gets one.
124 if (!dclFired) {
125 pending++;
126 onRealDcl = function () {
127 onRealDcl = 0;
128 Promise.resolve().then(function () {
129 if (!phase) done();
130 });
131 };
132 }
133
134 // A readystatechange 'complete' still to come is forwarded to the
135 // private name when it happens. This is registered before our wrappers,
136 // so it goes on the real name. A handler a delayed script set on
137 // document.onreadystatechange before the real 'interactive' hears it
138 // from the browser, so the synthetic one skips it.
139 if (!pageLoaded) {
140 document.addEventListener('readystatechange', function () {
141 if (realReadyState() === 'interactive') currentRsc = document.onreadystatechange;
142 if (realReadyState() === 'complete') firePrivate(document, 'readystatechange');
143 });
144 }
145
146 // Scripts already on the page keep the real event names and the native
147 // document.write for their own top-level code: a page defer script's
148 // readystatechange listener hears only the real one, and the browser
149 // ignores their writes after parsing as it always did.
150 Array.prototype.forEach.call(document.scripts, function (script) {
151 if (!script.hasAttribute('data-xs-delay')) script._xe = 1;
152 });
153
154 // The wrappers. What is renamed is decided on each call, by which events
155 // have really fired: DOMContentLoaded once it has, readystatechange once
156 // parsing has finished, window load once the page has loaded. A listener
157 // for an event still to come stays on the real name and hears the real
158 // event, whoever adds it; one for an event already gone gets a private
159 // copy. Renaming everything from the start was worse: eager scripts
160 // still registering had their listeners renamed, jQuery's completed()
161 // ran twice, and the Interactivity API's hydration was held until the
162 // replay ended.
163 //
164 // load is renamed on window only. The page's load never reaches a
165 // listener on document, so one there catches its descendants' loads
166 // in the capture phase (image delegation); renaming it would cut the
167 // delayed script off from every image after this.
168 //
169 // What renaming cannot tell apart: eager code in a timer or handler (no
170 // currentScript) registering during the replay gets one synthetic event
171 // where it would have got none.
172 //
173 // The wrappers look the prototype method up on every call: Sentry or
174 // zone.js, delayed too, can patch EventTarget.prototype during the
175 // replay, and listeners added after that must go through their patch.
176 // A removal takes the listener off both the real and the private name,
177 // so one added before an event fired and removed after (jQuery's
178 // completed()) is still found.
179 [document, window].forEach(function (target) {
180 var proto = Object.getPrototypeOf(target);
181 var ownAdd = target.addEventListener;
182 var ownRemove = target.removeEventListener;
183 var hadOwn = Object.prototype.hasOwnProperty.call(target, 'addEventListener');
184 var nameFor = function (type) {
185 var current = document.currentScript;
186 var gone =
187 type === 'load'
188 ? target === window && loadFired
189 : type === 'DOMContentLoaded'
190 ? dclFired
191 : realReadyState() !== 'loading';
192 return live && !(current && current._xe) && PRIVATE.hasOwnProperty(type) && gone ? PRIVATE[type] : type;
193 };
194 var add = function (type, listener, options) {
195 return (hadOwn ? ownAdd : proto.addEventListener).call(this, nameFor(type), listener, options);
196 };
197 target.addEventListener = add;
198 target.removeEventListener = function (type, listener, options) {
199 var remove = hadOwn ? ownRemove : proto.removeEventListener;
200 if (PRIVATE.hasOwnProperty(type)) remove.call(this, PRIVATE[type], listener, options);
201 return remove.call(this, type, listener, options);
202 };
203 wrapped.push([target, ownAdd, ownRemove, hadOwn, add]);
204 });
205
206 // window.onload / document.onreadystatechange set during the replay are
207 // keyed on the real names, so they are called directly. A page handler
208 // already there is swapped for an empty one first: the old addLoadEvent
209 // chain (`var o=window.onload; window.onload=function(){o();mine();}`)
210 // would otherwise run it a second time. It has run, and the browser
211 // will not call it again. If nothing replaced the empty one, the page's
212 // handler is put back at the end, so code that calls window.onload()
213 // later (a PJAX re-init) still finds it.
214 var pageOnload = window.onload;
215 var pageRsc = document.onreadystatechange;
216 var currentOnload = pageOnload;
217 var currentRsc = pageRsc;
218 var jq = window.jQuery;
219 var ended;
220 if (pageLoaded && pageOnload) currentOnload = window.onload = function () {};
221 if (pageLoaded && pageRsc) currentRsc = document.onreadystatechange = function () {};
222
223 // The replayed scripts read a fake document.readyState: 'loading' while
224 // they run, then 'interactive' for the synthetic DOMContentLoaded, then
225 // the real value once the replay ends. Left at the real value, a script
226 // that starts at once when the page is past 'loading' and also adds an
227 // unguarded DOMContentLoaded listener started twice, and a
228 // readystatechange handler waiting for 'interactive' never ran.
229 //
230 // The fake is scoped: the getter answers with it only while one of our
231 // clones runs (currentScript carries _xs), while an inline module runs
232 // between its markers (modules have no currentScript), and while our own
233 // dispatch runs (fakeDepth). Everything else reads the real value: eager
234 // code, timers, callbacks, and scripts the page or the replayed ones
235 // inject. So a delayed script that listens at top level and re-checks
236 // for 'complete' in a timer starts twice.
237 //
238 // The define is configurable and in a try: WP Rocket's first fake threw
239 // where Cloudflare Rocket Loader had locked the property (#5709), and a
240 // locked readyState keeps the old behaviour here. A getter the page
241 // defined itself (another optimizer, a polyfill) is read through and
242 // put back, not deleted.
243 var writtenPending = 0;
244 var fakeState = 'loading';
245 var fakeDepth = 0;
246 var ownReadyState = Object.getOwnPropertyDescriptor(document, 'readyState');
247 var faking =
248 nativeReadyState &&
249 (function () {
250 try {
251 Object.defineProperty(document, 'readyState', {
252 configurable: true,
253 get: function () {
254 var current = document.currentScript;
255 if ((current && current._xs) || fakeDepth) return fakeState;
256 return ownReadyState && ownReadyState.get ? ownReadyState.get.call(document) : realReadyState();
257 },
258 });
259 return 1;
260 } catch (e) {}
261 })();
262
263 // With readyState faked, a script may write into the page as if it were
264 // still being parsed. From an inline script that would wipe the
265 // document, so for the length of the replay document.write/writeln put
266 // the markup in after the script that wrote it, in call order. A script
267 // the parser is still running writes into the parser as always: while
268 // the real readyState is 'loading', a caller that is not one of our
269 // clones gets the native write. With no currentScript (a timer, a
270 // callback) there is nowhere to put it, and the write is dropped rather
271 // than wiping the page.
272 //
273 // A written <script src> runs whenever it arrives: fragment scripts are
274 // async. One written that way (an ad tag's second stage) writes through
275 // the redirect too, even while the page still parses, and the redirect
276 // stays until every written <script src> has loaded or failed, however
277 // long after the replay that is. Each of write and writeln is put back
278 // only if it is still ours: an ad loader may have installed its own.
279 var nativeWrite = [
280 document.write,
281 document.writeln,
282 Object.prototype.hasOwnProperty.call(document, 'write'),
283 ];
284 function redirectWrite(args, end) {
285 var current = document.currentScript;
286 var html = Array.prototype.join.call(args, '') + end;
287 if (!current) return;
288 if (current._xe || (!current._xs && !current._xf && realReadyState() === 'loading')) {
289 return (end ? nativeWrite[1] : nativeWrite[0]).apply(document, args);
290 }
291 if (!current.parentNode) return;
292 if (!('_xw' in current)) current._xw = current.nextSibling;
293 try {
294 var fragment = document.createRange().createContextualFragment(html);
295 Array.prototype.forEach.call(fragment.querySelectorAll('script'), function (script) {
296 script._xf = 1;
297 if (!script.src) return;
298 writtenPending++;
299 var settled = 0;
300 var settle = function () {
301 if (settled) return;
302 settled = 1;
303 writtenPending--;
304 if (ended && !heldInlines && !writtenPending) restoreWrite();
305 };
306 script.addEventListener('load', settle);
307 script.addEventListener('error', settle);
308 });
309 current.parentNode.insertBefore(fragment, current._xw);
310 } catch (e) {}
311 }
312 var ourWrite = (document.write = function () {
313 redirectWrite(arguments, '');
314 });
315 var ourWriteln = (document.writeln = function () {
316 redirectWrite(arguments, '\n');
317 });
318 function restoreWrite() {
319 if (document.write === ourWrite) {
320 if (nativeWrite[2]) document.write = nativeWrite[0];
321 else delete document.write;
322 }
323 if (document.writeln === ourWriteln) {
324 if (nativeWrite[2]) document.writeln = nativeWrite[1];
325 else delete document.writeln;
326 }
327 }
328
329 function fireReadyStateChange() {
330 firePrivate(document, 'readystatechange');
331 if (document.onreadystatechange !== currentRsc) {
332 callHandler(document.onreadystatechange, document, 'readystatechange');
333 }
334 }
335
336 // jQuery keeps ONE native listener per element and type. If window load
337 // already had jQuery handlers before the replay, that listener is on the
338 // real name and the private dispatch cannot reach handlers appended to
339 // the list, so those, and only those, are called directly in finish().
340 function jqueryLoadHandlers() {
341 var events = jq && jq._data && jq._data(window, 'events');
342 return (events && events.load) || [];
343 }
344 var jqueryLoadCount = jqueryLoadHandlers().length;
345
346 // Called once per thing the current phase waits for. Order matches a
347 // real page: readystatechange ('interactive'), DOMContentLoaded
348 // (bubbles document -> window, so it is not also dispatched on window),
349 // readystatechange ('complete'), load.
350 //
351 // Between DOMContentLoaded and load there is a second wait. On a real
352 // page a script inserted before load delays it, so a script that a
353 // DOMContentLoaded handler or jQuery ready code injects (GTM's DOM Ready
354 // trigger) always hears load. So the observer stays on, and load waits
355 // for what it sees. A wait left over from the first phase (the deadline
356 // cut it short) is ignored when it ends, and so are module markers.
357 //
358 // load also waits for jQuery's ready callbacks. On a real page ready
359 // runs before load, so `jQuery(function(){ $(window).on('load', f) })`,
360 // the common WordPress pattern, gets f. jQuery 3 runs ready callbacks on
361 // timers, so a callback queued after the delayed scripts' own, then one
362 // more timer, is when they have all run. 1s caps it (jQuery.holdReady
363 // can hold ready indefinitely); the callback and the cap share one slot.
364 function done() {
365 if (pending < 1 || --pending) return;
366 if (phase) return finish();
367 phase = 1;
368 clearTimeout(deadline);
369 for (var i in modules) if (modules[i] === 2) fakeDepth--;
370 modules = {};
371 if (faking) {
372 fakeState = 'interactive';
373 fakeDepth++;
374 fireReadyStateChange();
375 }
376 firePrivate(document, 'DOMContentLoaded', 1);
377 if (faking) fakeDepth--;
378 if (pageLoaded && !faking) fireReadyStateChange();
379 pending = 1;
380 var $ = window.jQuery;
381 var once = 0;
382 var ready = function () {
383 if (!once) {
384 once = 1;
385 done();
386 }
387 };
388 try {
389 if ($ && $.fn && $.fn.ready) {
390 $(function () {
391 setTimeout(ready, 0);
392 });
393 setTimeout(ready, 1000);
394 return;
395 }
396 } catch (e) {}
397 setTimeout(ready, 0);
398 }
399
400 // Restore puts back what was there (delete our own-property shadow, or
401 // reassign a shadow someone set before us) unless another wrapper has
402 // since been put on top; `live` makes ours inert either way.
403 function finish() {
404 if (ended) return;
405 ended = 1;
406 if (observer) observer.disconnect();
407 if (faking) {
408 if (ownReadyState) Object.defineProperty(document, 'readyState', ownReadyState);
409 else delete document.readyState;
410 if (pageLoaded) fireReadyStateChange();
411 }
412 if (!heldInlines && !writtenPending) restoreWrite();
413 live = false;
414 wrapped.forEach(function (entry) {
415 var target = entry[0];
416 if (target.addEventListener !== entry[4]) return;
417 if (entry[3]) {
418 target.addEventListener = entry[1];
419 target.removeEventListener = entry[2];
420 } else {
421 delete target.addEventListener;
422 delete target.removeEventListener;
423 }
424 });
425 if (loadFired) firePrivate(window, 'load');
426 if (pageLoaded) {
427 if (window.onload !== currentOnload) callHandler(window.onload, window, 'load');
428 if (jqueryLoadCount) {
429 jqueryLoadHandlers()
430 .slice(jqueryLoadCount)
431 .forEach(function (handleObj) {
432 try {
433 var event = jq.Event('load');
434 event.currentTarget = window;
435 event.handleObj = handleObj;
436 event.data = handleObj.data;
437 handleObj.handler.call(window, event);
438 } catch (e) {}
439 });
440 }
441 }
442 if (pageOnload && window.onload === currentOnload) window.onload = pageOnload;
443 if (pageLoaded && pageRsc && document.onreadystatechange === currentRsc) document.onreadystatechange = pageRsc;
444 // Marks the end, for tests and integrators.
445 document.dispatchEvent(new Event('xspeed:replayed'));
446 }
447
448 // Wait for a script's load or error: listeners, never n.onload, which
449 // would replace an author's copied onload attribute. They go on before
450 // insertion, which is what starts the fetch.
451 function wait(script, cap) {
452 pending++;
453 var settled = 0;
454 var timer;
455 var inPhase = phase;
456 function one() {
457 if (settled) return;
458 settled = 1;
459 clearTimeout(timer);
460 if (inPhase === phase) done();
461 }
462 script.addEventListener('load', one);
463 script.addEventListener('error', one);
464 if (cap) timer = setTimeout(one, cap);
465 }
466
467 // A script a replayed script injects (a tag manager's payload, a widget's
468 // real code) is waited for too, 1s each: the cap WP Rocket and WP Meteor
469 // use, because some never fire load or error (one added through
470 // innerHTML never runs). A script injected later than that gets no
471 // events. Our own clones carry _xs, so they are not counted twice.
472 function seen(script) {
473 if (!script._xs && script.hasAttribute('src') && willRun(script, typeOf(script))) wait(script, 1000);
474 }
475 var observer =
476 window.MutationObserver &&
477 new MutationObserver(function (records) {
478 records.forEach(function (record) {
479 Array.prototype.forEach.call(record.addedNodes, function (node) {
480 if (node.nodeName === 'SCRIPT') seen(node);
481 else if (node.querySelectorAll) Array.prototype.forEach.call(node.querySelectorAll('script'), seen);
482 });
483 });
484 });
485 if (observer) observer.observe(document.documentElement, { childList: true, subtree: true });
486
487 // A server that never answers does not hold the events past 15s.
488 deadline = setTimeout(function () {
489 pending = 1;
490 done();
491 }, 15000);
492
493 // An inline module evaluates asynchronously but fires no load (the spec
494 // fires it only for scripts from a URL), so its replayed text starts and
495 // ends with a line that dispatches xs-mod with its index, and the end is
496 // counted in pending. Its imports are hoisted, so the end runs once the
497 // module and its whole import graph have run. An import that fails to
498 // load fires error on the element, which also counts.
499 //
500 // A module that throws never reaches its end line, but its start line
501 // ran and the throw is reported to window: an error while a module is
502 // between its markers releases that module only. The release runs one
503 // microtask after the error: an error thrown by another listener while
504 // the module is still running is reported synchronously, and by the
505 // microtask that module has reached its end line. A module paused at a
506 // top-level await is between its markers too, so an unrelated error
507 // during the pause releases it early; a precise rule costs more than it
508 // saves. A dependency that throws stops the module before its start
509 // line; the deadline covers that. A module's end is counted a microtask
510 // later, so a script it injects is seen first.
511 function moduleDone(index) {
512 if (modules[index]) {
513 if (modules[index] === 2) fakeDepth--;
514 modules[index] = 0;
515 Promise.resolve().then(done);
516 }
517 }
518 document.addEventListener('xs-mod', function (event) {
519 var detail = event.detail;
520 if (detail > 0) moduleDone(detail);
521 else if (modules[-detail] === 1) {
522 modules[-detail] = 2;
523 fakeDepth++;
524 }
525 });
526 window.addEventListener('error', function () {
527 Promise.resolve().then(function () {
528 for (var i in modules) if (modules[i] === 2) moduleDone(i);
529 });
530 });
531
532 // Build the executable copy of a parked tag. index is its 1-based place
533 // in the loop, or 0 for a copy nothing waits for.
534 function clone(parked, index) {
535 var script = document.createElement('script');
536 script._xs = 1;
537 // A dynamically-created script is async by default, so replayed
538 // externals would race each other; async=false restores document
539 // order among them.
540 script.async = false;
541 // Nonce hiding: a connected element's nonce content attribute reads as
542 // "", so copying it through the attribute loop would hand the clone an
543 // empty nonce and a nonce CSP would block it. The IDL property still
544 // carries the real value.
545 if (parked.nonce) script.nonce = parked.nonce;
546 Array.prototype.slice.call(parked.attributes).forEach(function (attr) {
547 if (attr.name === 'data-xs-src') {
548 script.setAttribute('src', attr.value);
549 return;
550 }
551 if (attr.name === 'data-xs-delay') return;
552 if (attr.name === 'nonce') return;
553 // A parked inline tag's original type (module, mostly) rides in
554 // data-xs-type: restore it, or a module runs as a classic script and
555 // its imports throw (#274).
556 if (attr.name === 'data-xs-type') {
557 script.setAttribute('type', attr.value);
558 return;
559 }
560 // type is what a script IS, so it is carried over, except our own
561 // parking marker, which exists only to stop the browser running the
562 // original. Dropping type wholesale made type="module" a classic
563 // script and made a consent manager's type="text/plain" executable
564 // again, which is a privacy failure (#274).
565 if (attr.name === 'type' && attr.value === 'text/xspeed-delayed') return;
566 script.setAttribute(attr.name, attr.value);
567 });
568 if (!parked.hasAttribute('data-xs-src')) script.text = parked.text;
569 var type = typeOf(script);
570 if (index && willRun(script, type)) {
571 if (script.hasAttribute('src')) wait(script);
572 else if (type === 'module') {
573 pending++;
574 modules[index] = 1;
575 script.text =
576 "document.dispatchEvent(new CustomEvent('xs-mod',{detail:-" +
577 index +
578 '}));' +
579 script.text +
580 "\n;document.dispatchEvent(new CustomEvent('xs-mod',{detail:" +
581 index +
582 '}))';
583 script.addEventListener('error', function () {
584 moduleDone(index);
585 });
586 // A CSP that lists inline scripts by hash blocks the changed text,
587 // so on an enforced violation the untouched original goes in
588 // instead, unwaited for. Report-only violations are ignored: that
589 // copy ran.
590 script.addEventListener('securitypolicyviolation', function (event) {
591 if (event.disposition === 'enforce' && event.blockedURI === 'inline' && script.parentNode) {
592 script.parentNode.replaceChild(clone(parked), script);
593 moduleDone(index);
594 }
595 });
596 }
597 }
598 return script;
599 }
600
601 // The loop. An inline script runs the moment it is inserted, so one
602 // after a delayed external would run before that external arrived
603 // (`jQuery(function(){...})` after a delayed jQuery threw). An inline
604 // that follows a classic, non-async, non-defer executable external is
605 // inserted from that external's load/error listener instead. The
606 // async=false externals run from one ordered list, and the spec fires
607 // each one's load right after it runs and before the next starts, so the
608 // inline runs exactly between them; the externals are still all inserted
609 // at once, so they download in parallel. An async or defer external and
610 // a module hold nothing: on a real page none of them blocks the inline
611 // after it.
612 //
613 // A held inline's text is unchanged, so a hash CSP still allows it. Its
614 // hold is released a microtask after it goes in, so a script it injects
615 // is counted first. A hold released after the deadline no longer counts
616 // toward any wait, but the write redirect stays until the last one has
617 // gone in. Each tag is cloned and inserted inside its own try, so one
618 // the browser refuses costs that script only.
619 //
620 // A parked tag an earlier replayed script took out of the document is
621 // skipped: with no parent, replaceChild throws, and inside a detached
622 // subtree the clone never loads. contains(), not isConnected, which
623 // older engines lack.
624 var lastBlocking;
625 document.querySelectorAll('script[data-xs-delay]').forEach(function (parked, k) {
626 if (!document.documentElement.contains(parked)) return;
627 if (lastBlocking && !parked.hasAttribute('data-xs-src')) {
628 pending++;
629 heldInlines++;
630 var released = 0;
631 var inPhase = phase;
632 var release = function () {
633 if (released) return;
634 released = 1;
635 heldInlines--;
636 try {
637 if (document.documentElement.contains(parked)) {
638 parked.parentNode.replaceChild(clone(parked, inPhase === phase ? k + 1 : 0), parked);
639 }
640 } catch (e) {}
641 if (ended && !heldInlines && !writtenPending) restoreWrite();
642 if (inPhase === phase) Promise.resolve().then(done);
643 };
644 lastBlocking.addEventListener('load', release);
645 lastBlocking.addEventListener('error', release);
646 return;
647 }
648 try {
649 var script = clone(parked, k + 1);
650 parked.parentNode.replaceChild(script, parked);
651 } catch (e) {
652 return;
653 }
654 var type = typeOf(script);
655 if (
656 script.hasAttribute('src') &&
657 type !== 'module' &&
658 !script.async &&
659 !script.hasAttribute('defer') &&
660 willRun(script, type)
661 ) {
662 lastBlocking = script;
663 }
664 });
665 // An inline loader (GTM, gtag, the Meta pixel) injects during the loop,
666 // and the observer's records for it arrive in the microtask queued
667 // before this one.
668 Promise.resolve().then(done);
669 }
670
671 TRIGGERS.forEach(function (name) {
672 window.addEventListener(name, start, { passive: true, capture: true });
673 });
674 // The failsafe timer, for visitors who never interact. 0 means
675 // interaction only.
676 if (timeout > 0) setTimeout(start, timeout);
677 })(XSPEED_DELAY_TIMEOUT);
678