PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.2
1.4.2 1.4.1 1.4.0 1.3.7 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 All 36 releases
xspeed / includes / js / delay-bootstrap.js

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

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