nodebb-plugin-ezoic-infinite 1.7.12 → 1.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +131 -124
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.12",
3
+ "version": "1.7.14",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,38 +1,53 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v23
2
+ * NodeBB Ezoic Infinite Ads — client.js v24
3
3
  *
4
- * v20 Table KIND : anchorAttr par kindClass. Fix catégories (data-cid).
5
- * IO fixe une seule instance. Burst cooldown 200ms.
6
- * v22 Fix ordinal fallback posts (baseTag vide cassait :scope>). isFilled guard pruneOrphans.
7
- * v23 Fix "pubs écrasées" : INJECT_GRACE_MS protège les wraps non encore fills.
8
- * decluster : ne supprime jamais un wrap filled, grace basée sur A_CREATED aussi.
9
- * KIND table : baseTag explicite (évite le split fragile).
10
- * wrapByKey Map O(1) pour findWrap. poolsReady (initPools une fois/page).
11
- * patchShowAds sorti du hot path runCore. wrapByKey sync sur dropWrap/cleanup.
4
+ * Historique des corrections majeures
5
+ * ────────────────────────────────────
6
+ * v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
7
+ * Suppression du recyclage de wraps (moveWrapAfter). Cleanup complet navigation.
8
+ *
9
+ * v19 Intervalle global basé sur l'ordinal absolu (data-index) et non sur
10
+ * la position dans le batch courant.
11
+ *
12
+ * v20 Table KIND : anchorAttr/ordinalAttr/baseTag explicites par kindClass.
13
+ * Fix fatal catégories : data-cid au lieu de data-index inexistant.
14
+ * Fix posts : baseTag vide → ordinal fallback cassé → pubs jamais injectées.
15
+ * IO fixe (une instance, jamais recréée). Burst cooldown 200 ms.
16
+ * Fix unobserve(null) → corruption IO → pubads error au scroll retour.
17
+ * Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
18
+ *
19
+ * v21 Suppression de toute la logique wyvern.js (pause/destroy avant remove) :
20
+ * les erreurs wyvern viennent du SDK Ezoic lui-même lors de ses propres
21
+ * refreshes internes, pas de nos suppressions. Nos wraps filled ne sont
22
+ * de toute façon jamais supprimés (règle pruneOrphans/decluster).
23
+ * Refactorisation finale prod-ready : code unifié, zéro duplication,
24
+ * commentaires essentiels uniquement.
12
25
  */
13
26
  (function () {
14
27
  'use strict';
15
28
 
16
29
  // ── Constantes ─────────────────────────────────────────────────────────────
17
30
 
18
- const WRAP_CLASS = 'nodebb-ezoic-wrap';
19
- const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
20
- const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
21
- const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
22
- const A_CREATED = 'data-ezoic-created'; // timestamp création ms
23
- const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
24
-
25
- const MIN_PRUNE_AGE_MS = 8_000; // délai avant qu'un wrap puisse être purgé
26
- const INJECT_GRACE_MS = 30_000; // fenêtre post-injection : wrap non supprimable (fill async en cours)
27
- const FILL_GRACE_MS = 25_000; // fenêtre post-showAds pour decluster
28
- const EMPTY_CHECK_MS = 20_000; // délai avant de marquer un wrap vide
29
- const MAX_INSERTS_PER_RUN = 6;
30
- const MAX_INFLIGHT = 4;
31
- const SHOW_THROTTLE_MS = 900;
32
- const BURST_COOLDOWN_MS = 200;
33
-
34
- const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
35
- const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
31
+ const WRAP_CLASS = 'nodebb-ezoic-wrap';
32
+ const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
33
+ const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
34
+ const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
35
+ const A_CREATED = 'data-ezoic-created'; // timestamp création ms
36
+ const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
37
+
38
+ const MIN_PRUNE_AGE_MS = 8_000; // garde-fou post-batch NodeBB
39
+ const PRUNE_STABLE_MS = 45_000; // délai avant qu'un wrap vide puisse être purgé
40
+ // (évite la suppression lors du scroll up / virtualisation NodeBB)
41
+ const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
42
+ const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
43
+ const MAX_INSERTS_RUN = 6;
44
+ const MAX_INFLIGHT = 4;
45
+ const SHOW_THROTTLE_MS = 900;
46
+ const BURST_COOLDOWN_MS = 200;
47
+
48
+ // IO : marges larges fixes — une seule instance, jamais recréée
49
+ const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
50
+ const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
36
51
 
37
52
  const SEL = {
38
53
  post: '[component="post"][data-pid]',
@@ -43,46 +58,46 @@
43
58
  /**
44
59
  * Table KIND — source de vérité par kindClass.
45
60
  *
46
- * anchorAttr : attribut DOM stable → clé unique du wrap
47
- * data-pid posts (id message, immuable)
48
- * data-index topics (index dans la liste)
49
- * data-cid catégories (id catégorie, immuable)
50
- * baseTag : préfixe tag pour le querySelector d'ancre dans pruneOrphans.
51
- * Vide pour posts (sélecteur sans tag). Explicite pour éviter le split fragile.
61
+ * sel : sélecteur CSS complet
62
+ * baseTag : préfixe tag pour les querySelector de recherche d'ancre
63
+ * (vide pour posts car leur sélecteur commence par '[')
64
+ * anchorAttr : attribut DOM STABLE → clé unique du wrap, permanent
65
+ * data-pid posts (id message, immuable)
66
+ * data-index topics (index dans la liste)
67
+ * data-cid catégories (id catégorie, immuable)
68
+ * ordinalAttr: attribut 0-based pour le calcul de l'intervalle
69
+ * data-index posts + topics (fourni par NodeBB)
70
+ * null catégories (page statique → fallback positionnel)
52
71
  */
53
72
  const KIND = {
54
- 'ezoic-ad-message': { sel: SEL.post, anchorAttr: 'data-pid', baseTag: '' },
55
- 'ezoic-ad-between': { sel: SEL.topic, anchorAttr: 'data-index', baseTag: 'li' },
56
- 'ezoic-ad-categories': { sel: SEL.category, anchorAttr: 'data-cid', baseTag: 'li' },
73
+ 'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
74
+ 'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
75
+ 'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
57
76
  };
58
77
 
59
78
  // ── État ───────────────────────────────────────────────────────────────────
60
79
 
61
80
  const S = {
62
- pageKey: null,
63
- cfg: null,
64
- pools: { topics: [], posts: [], categories: [] },
65
- cursors: { topics: 0, posts: 0, categories: 0 },
66
- mountedIds: new Set(), // IDs Ezoic dans le DOM
67
- lastShow: new Map(), // id → timestamp dernier show
68
- io: null,
69
- domObs: null,
70
- mutGuard: 0,
71
- inflight: 0,
72
- pending: [],
73
- pendingSet: new Set(),
74
- runQueued: false,
75
- burstActive: false,
81
+ pageKey: null,
82
+ cfg: null,
83
+ pools: { topics: [], posts: [], categories: [] },
84
+ cursors: { topics: 0, posts: 0, categories: 0 },
85
+ mountedIds: new Set(), // IDs Ezoic montés dans le DOM
86
+ lastShow: new Map(), // id → timestamp dernier show
87
+ io: null,
88
+ domObs: null,
89
+ mutGuard: 0,
90
+ inflight: 0,
91
+ pending: [],
92
+ pendingSet: new Set(),
93
+ runQueued: false,
94
+ burstActive: false,
76
95
  burstDeadline: 0,
77
- burstCount: 0,
78
- lastBurstTs: 0,
96
+ burstCount: 0,
97
+ lastBurstTs: 0,
79
98
  };
80
99
 
81
- // Map anchorKey → wrap Element — lookup O(1) au lieu de querySelector full-DOM
82
- const wrapByKey = new Map();
83
- let blockedUntil = 0;
84
- let poolsReady = false; // initPools une seule fois par page
85
-
100
+ let blockedUntil = 0;
86
101
  const ts = () => Date.now();
87
102
  const isBlocked = () => ts() < blockedUntil;
88
103
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
@@ -115,11 +130,9 @@
115
130
  }
116
131
 
117
132
  function initPools(cfg) {
118
- if (poolsReady) return;
119
133
  S.pools.topics = parseIds(cfg.placeholderIds);
120
134
  S.pools.posts = parseIds(cfg.messagePlaceholderIds);
121
135
  S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
122
- poolsReady = true;
123
136
  }
124
137
 
125
138
  // ── Page identity ──────────────────────────────────────────────────────────
@@ -182,13 +195,14 @@
182
195
  return 'i0';
183
196
  }
184
197
 
185
- const makeAnchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
198
+ const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
186
199
 
187
200
  function findWrap(key) {
188
- const w = wrapByKey.get(key);
189
- if (w && w.isConnected) return w;
190
- if (w) wrapByKey.delete(key); // nettoyage lazy si wrap retiré hors de notre contrôle
191
- return null;
201
+ try {
202
+ return document.querySelector(
203
+ `.${WRAP_CLASS}[${A_ANCHOR}="${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
204
+ );
205
+ } catch (_) { return null; }
192
206
  }
193
207
 
194
208
  // ── Pool ───────────────────────────────────────────────────────────────────
@@ -207,7 +221,7 @@
207
221
  // ── Wraps DOM ──────────────────────────────────────────────────────────────
208
222
 
209
223
  function makeWrap(id, klass, key) {
210
- const w = document.createElement('div');
224
+ const w = document.createElement('div');
211
225
  w.className = `${WRAP_CLASS} ${klass}`;
212
226
  w.setAttribute(A_ANCHOR, key);
213
227
  w.setAttribute(A_WRAPID, String(id));
@@ -221,28 +235,24 @@
221
235
  }
222
236
 
223
237
  function insertAfter(el, id, klass, key) {
224
- if (!el?.insertAdjacentElement) return null;
225
- if (findWrap(key)) return null;
226
- if (S.mountedIds.has(id)) return null;
227
- if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
238
+ if (!el?.insertAdjacentElement) return null;
239
+ if (findWrap(key)) return null;
240
+ if (S.mountedIds.has(id)) return null;
241
+ if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
228
242
  const w = makeWrap(id, klass, key);
229
243
  mutate(() => el.insertAdjacentElement('afterend', w));
230
244
  S.mountedIds.add(id);
231
- wrapByKey.set(key, w);
232
245
  return w;
233
246
  }
234
247
 
235
248
  function dropWrap(w) {
236
249
  try {
250
+ // Unobserve avant remove — guard instanceof évite unobserve(null)
251
+ // qui corrompt l'état interne de l'IO (pubads error au scroll suivant)
252
+ const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
253
+ if (ph instanceof Element) S.io?.unobserve(ph);
237
254
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
238
255
  if (Number.isFinite(id)) S.mountedIds.delete(id);
239
- const key = w.getAttribute(A_ANCHOR);
240
- if (key) wrapByKey.delete(key);
241
- // unobserve avant remove — guard instanceof (unobserve(null) corrompt l'IO pubads)
242
- try {
243
- const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
244
- if (ph instanceof Element) S.io?.unobserve(ph);
245
- } catch (_) {}
246
256
  w.remove();
247
257
  } catch (_) {}
248
258
  }
@@ -252,20 +262,24 @@
252
262
  /**
253
263
  * Supprime les wraps VIDES dont l'ancre a disparu du DOM.
254
264
  *
255
- * Protections :
256
- * 1. Pas avant MIN_PRUNE_AGE_MS (DOM post-batch pas encore stabilisé)
257
- * 2. Jamais si filled (pub affichée SDK Ezoic a des callbacks async dessus)
258
- * 3. Pas avant INJECT_GRACE_MS depuis création (fill Ezoic peut être très async)
265
+ * On ne supprime JAMAIS un wrap rempli (filled) :
266
+ * - Les wraps remplis peuvent être temporairement orphelins lors d'une
267
+ * virtualisation NodeBBl'ancre reviendra.
268
+ * - Le SDK Ezoic (wyvern, GAM) exécute des callbacks async sur le contenu ;
269
+ * retirer le nœud sous ses pieds génère des erreurs non critiques mais
270
+ * inutiles. Le cleanup de navigation gère la suppression définitive.
259
271
  */
260
272
  function pruneOrphans(klass) {
261
273
  const meta = KIND[klass];
262
274
  if (!meta) return;
263
275
 
264
276
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
265
- const age = ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10);
266
- if (age < MIN_PRUNE_AGE_MS) continue;
267
- if (isFilled(w)) continue;
268
- if (age < INJECT_GRACE_MS) continue;
277
+ // Ne jamais supprimer un wrap filled
278
+ if (isFilled(w)) continue;
279
+ // Attendre PRUNE_STABLE_MS depuis la création : pendant ce délai, l'ancre
280
+ // peut avoir temporairement disparu du DOM par virtualisation NodeBB au
281
+ // scroll up — ce n'est pas un vrai orphelin.
282
+ if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
269
283
 
270
284
  const key = w.getAttribute(A_ANCHOR) ?? '';
271
285
  const sid = key.slice(klass.length + 1);
@@ -281,18 +295,15 @@
281
295
 
282
296
  /**
283
297
  * Deux wraps adjacents → supprimer le moins prioritaire.
284
- *
285
- * Règles absolues :
286
- * - Jamais supprimer un wrap filled
287
- * - Jamais supprimer un wrap < INJECT_GRACE_MS (fill Ezoic potentiellement en cours)
288
- * - Jamais supprimer un wrap < FILL_GRACE_MS depuis le dernier showAds
289
- * - Si les deux wraps adjacents sont vides et hors grâce → supprimer le courant
298
+ * Priorité : filled > en grâce de fill > vide.
299
+ * Jamais de suppression d'un wrap rempli (même raison que pruneOrphans).
290
300
  */
291
301
  function decluster(klass) {
292
302
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
303
+ // Ne jamais toucher un wrap filled
293
304
  if (isFilled(w)) continue;
294
- const wAge = ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10);
295
- if (wAge < INJECT_GRACE_MS) continue;
305
+ // Protéger par A_CREATED : un wrap récent attend encore son showAds async
306
+ if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
296
307
  const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
297
308
  if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
298
309
 
@@ -300,8 +311,7 @@
300
311
  while (prev && steps++ < 3) {
301
312
  if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
302
313
  if (isFilled(prev)) break;
303
- const pAge = ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10);
304
- if (pAge < INJECT_GRACE_MS) break;
314
+ if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
305
315
  const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
306
316
  if (pShown && ts() - pShown < FILL_GRACE_MS) break;
307
317
 
@@ -316,13 +326,16 @@
316
326
 
317
327
  /**
318
328
  * Ordinal 0-based pour le calcul de l'intervalle.
319
- * Posts/topics : data-index (NodeBB 4.x).
320
- * Catégories : fallback positionnel (page statique, pas d'infinite scroll).
329
+ * Posts et topics : data-index (NodeBB 4.x, stable entre les batches).
330
+ * Catégories : fallback positionnel (page statique, pas d'infinite scroll).
321
331
  */
322
332
  function ordinal(klass, el) {
323
- const di = el.getAttribute('data-index');
324
- if (di !== null && di !== '' && !isNaN(di)) return parseInt(di, 10);
325
- // Fallback s.matches(fullSel) pour ne pas compter les wraps intercalés
333
+ const attr = KIND[klass]?.ordinalAttr;
334
+ if (attr) {
335
+ const v = el.getAttribute(attr);
336
+ if (v !== null && v !== '' && !isNaN(v)) return parseInt(v, 10);
337
+ }
338
+ // Fallback positionnel — compte uniquement les éléments du même type
326
339
  const fullSel = KIND[klass]?.sel ?? '';
327
340
  let i = 0;
328
341
  for (const s of el.parentElement?.children ?? []) {
@@ -337,15 +350,14 @@
337
350
  let inserted = 0;
338
351
 
339
352
  for (const el of items) {
340
- if (inserted >= MAX_INSERTS_PER_RUN) break;
353
+ if (inserted >= MAX_INSERTS_RUN) break;
341
354
  if (!el?.isConnected) continue;
342
355
 
343
356
  const ord = ordinal(klass, el);
344
357
  if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
345
-
346
358
  if (adjacentWrap(el)) continue;
347
359
 
348
- const key = makeAnchorKey(klass, el);
360
+ const key = anchorKey(klass, el);
349
361
  if (findWrap(key)) continue;
350
362
 
351
363
  const id = pickId(poolKey);
@@ -481,6 +493,7 @@
481
493
 
482
494
  async function runCore() {
483
495
  if (isBlocked()) return 0;
496
+ patchShowAds();
484
497
 
485
498
  const cfg = await fetchConfig();
486
499
  if (!cfg || cfg.excluded) return 0;
@@ -506,14 +519,13 @@
506
519
  'ezoic-ad-between', getTopics,
507
520
  cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
508
521
  );
509
- if (kind === 'categories') return exec(
522
+ return exec(
510
523
  'ezoic-ad-categories', getCategories,
511
524
  cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
512
525
  );
513
- return 0;
514
526
  }
515
527
 
516
- // ── Scheduler / Burst ──────────────────────────────────────────────────────
528
+ // ── Scheduler ──────────────────────────────────────────────────────────────
517
529
 
518
530
  function scheduleRun(cb) {
519
531
  if (S.runQueued) return;
@@ -523,7 +535,7 @@
523
535
  if (S.pageKey && pageKey() !== S.pageKey) return;
524
536
  let n = 0;
525
537
  try { n = await runCore(); } catch (_) {}
526
- try { cb?.(n); } catch (_) {}
538
+ cb?.(n);
527
539
  });
528
540
  }
529
541
 
@@ -531,10 +543,8 @@
531
543
  if (isBlocked()) return;
532
544
  const t = ts();
533
545
  if (t - S.lastBurstTs < BURST_COOLDOWN_MS) return;
534
- S.lastBurstTs = t;
535
-
536
- const pk = pageKey();
537
- S.pageKey = pk;
546
+ S.lastBurstTs = t;
547
+ S.pageKey = pageKey();
538
548
  S.burstDeadline = t + 2000;
539
549
 
540
550
  if (S.burstActive) return;
@@ -542,7 +552,7 @@
542
552
  S.burstCount = 0;
543
553
 
544
554
  const step = () => {
545
- if (pageKey() !== pk || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
555
+ if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
546
556
  S.burstActive = false; return;
547
557
  }
548
558
  S.burstCount++;
@@ -558,9 +568,7 @@
558
568
 
559
569
  function cleanup() {
560
570
  blockedUntil = ts() + 1500;
561
- poolsReady = false;
562
571
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
563
- wrapByKey.clear();
564
572
  S.cfg = null;
565
573
  S.pools = { topics: [], posts: [], categories: [] };
566
574
  S.cursors = { topics: 0, posts: 0, categories: 0 };
@@ -573,7 +581,7 @@
573
581
  S.runQueued = false;
574
582
  }
575
583
 
576
- // ── DOM Observer ───────────────────────────────────────────────────────────
584
+ // ── MutationObserver ───────────────────────────────────────────────────────
577
585
 
578
586
  function ensureDomObserver() {
579
587
  if (S.domObs) return;
@@ -581,7 +589,6 @@
581
589
  S.domObs = new MutationObserver(muts => {
582
590
  if (S.mutGuard > 0 || isBlocked()) return;
583
591
  for (const m of muts) {
584
- if (!m.addedNodes?.length) continue;
585
592
  for (const n of m.addedNodes) {
586
593
  if (n.nodeType !== 1) continue;
587
594
  if (allSel.some(s => n.matches?.(s) || n.querySelector?.(s))) {
@@ -611,7 +618,8 @@
611
618
 
612
619
  function ensureTcfLocator() {
613
620
  // L'iframe __tcfapiLocator route les appels postMessage du CMP.
614
- // En navigation ajaxify, NodeBB peut la retirer → erreurs CMP.
621
+ // En navigation ajaxify, NodeBB peut la retirer du DOM → erreurs CMP.
622
+ // Un MutationObserver la recrée dès qu'elle disparaît.
615
623
  try {
616
624
  if (!window.__tcfapi && !window.__cmp) return;
617
625
  const inject = () => {
@@ -650,7 +658,7 @@
650
658
  }
651
659
  }
652
660
 
653
- // ── Bindings NodeBB ────────────────────────────────────────────────────────
661
+ // ── Bindings ───────────────────────────────────────────────────────────────
654
662
 
655
663
  function bindNodeBB() {
656
664
  const $ = window.jQuery;
@@ -661,16 +669,15 @@
661
669
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
662
670
  S.pageKey = pageKey();
663
671
  blockedUntil = 0;
664
- muteConsole(); ensureTcfLocator(); warmNetwork(); patchShowAds();
665
- getIO(); ensureDomObserver(); requestBurst();
672
+ muteConsole(); ensureTcfLocator(); warmNetwork();
673
+ patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
666
674
  });
667
675
 
668
- const BURST_EVENTS = [
669
- 'action:ajaxify.contentLoaded',
670
- 'action:posts.loaded', 'action:topics.loaded',
676
+ const burstEvts = [
677
+ 'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
671
678
  'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
672
679
  ].map(e => `${e}.nbbEzoic`).join(' ');
673
- $(window).on(BURST_EVENTS, () => { if (!isBlocked()) requestBurst(); });
680
+ $(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
674
681
 
675
682
  try {
676
683
  require(['hooks'], hooks => {