nodebb-plugin-ezoic-infinite 1.7.50 → 1.7.51

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 +91 -186
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.50",
3
+ "version": "1.7.51",
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,49 +1,41 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v51
2
+ * NodeBB Ezoic Infinite Ads — client.js v57
3
3
  *
4
- * Historique des corrections majeures
5
- * ────────────────────────────────────
6
- * v18 Ancrage stable par data-pid / data-index au lieu d'ordinalMap fragile.
7
- * v19 Intervalle global basé sur l'ordinal absolu (data-index).
8
- * v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
9
- * IO fixe (une instance, jamais recréée). Fix TCF locator.
4
+ * Historique
5
+ * ──────────
6
+ * v18 Ancrage stable par data-pid / data-index.
7
+ * v20 Table KIND. IO fixe. Fix TCF locator.
10
8
  * v25 Fix scroll-up / virtualisation NodeBB.
11
- * v26 Suppression recyclage d'id (causait réinjection en haut).
12
- * v27 pruneOrphans supprimé (faux-orphelins sur virtualisation posts).
13
- * v28 decluster supprimé. Wraps persistants pendant la session.
14
- * v32 pruneOrphansBetween réactivé uniquement pour topics de catégorie.
15
- * NodeBB NE virtualise PAS les topics → prune safe.
16
- * Toujours désactivé pour posts (virtualisation → faux-orphelins).
17
- * v35 initPools protégé (S.poolsReady). muteConsole élargi.
18
- * v36 S.wrapByKey Map O(1). wrapIsLive allégé. MutationObserver optimisé.
19
- * v38 ez.refresh() interdit → supprimé. Pool épuisé → break propre.
20
- * v40 Recyclage via destroyPlaceholders+define+displayMore (délais 300ms).
9
+ * v28 Wraps persistants pendant la session.
10
+ * v35 S.poolsReady. muteConsole élargi.
11
+ * v36 S.wrapByKey Map O(1). MutationObserver optimisé.
12
+ * v38 ez.refresh() supprimé. Pool épuisé break propre.
13
+ * v40 Recyclage destroyPlaceholders+define+displayMore (délais 300ms).
21
14
  * v43 Seuil recyclage -1vh + unobserve avant déplacement.
22
- * v49 Fix normalizeExcludedGroups : JSON.parse du tableau NodeBB.
23
- * v50 bindLoginCheck() supprimé (NodeBB recharge après login).
24
- * v51 #2 fetchConfig() : backoff 10s sur échec réseau (évite spam API).
25
- * #4 recycleAndMove : re-observe le placeholder après displayMore.
26
- * #5 ensureTcfLocator : MutationObserver global disconnecté au cleanup.
27
- * #6 getIO() recrée l'observer si le type d'écran change (resize).
15
+ * v49 Fix normalizeExcludedGroups (JSON.parse tableau NodeBB).
16
+ * v51 fetchConfig backoff 10s. IO recrée au resize. tcfObs dans S.
17
+ * v52 pruneOrphansBetween supprimé (NodeBB virtualise aussi les topics).
18
+ * v53 S.recycling garde double-recyclage. pickId early-exit. cleanup complet.
19
+ * v54 ensureTcfLocator rappelé à chaque ajaxify.end.
20
+ * v56 scheduleEmptyCheck / is-empty supprimés (collapse prématuré).
21
+ * v57 Nettoyage prod : A_SHOWN/A_CREATED supprimés, normBool Set O(1),
22
+ * muteConsole étend aux erreurs CMP getTCData, code nettoyé.
28
23
  */
29
24
  (function nbbEzoicInfinite() {
30
25
  'use strict';
31
26
 
32
27
  // ── Constantes ─────────────────────────────────────────────────────────────
33
28
 
34
- const WRAP_CLASS = 'nodebb-ezoic-wrap';
35
- const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
36
- const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
37
- const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
38
- const A_CREATED = 'data-ezoic-created'; // timestamp création ms
39
- const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
29
+ const WRAP_CLASS = 'nodebb-ezoic-wrap';
30
+ const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
31
+ const A_ANCHOR = 'data-ezoic-anchor'; // "kindClass:stableId"
32
+ const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
40
33
 
41
- const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
42
- const MAX_INFLIGHT = 4; // max showAds() simultanés
43
- const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
44
- const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
34
+ const MAX_INSERTS_RUN = 6; // insertions max par appel runCore
35
+ const MAX_INFLIGHT = 4; // showAds() simultanés max
36
+ const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
37
+ const BURST_COOLDOWN_MS = 200; // délai min entre deux bursts
45
38
 
46
- // Marges IO larges et fixes — observer créé une seule fois au boot
47
39
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
48
40
  const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
49
41
 
@@ -55,13 +47,10 @@
55
47
 
56
48
  /**
57
49
  * Table KIND — source de vérité par kindClass.
58
- *
59
- * sel sélecteur CSS complet des éléments cibles
50
+ * sel sélecteur CSS des éléments cibles
60
51
  * baseTag préfixe tag pour querySelector d'ancre
61
- * (vide pour posts : le sélecteur commence par '[')
62
52
  * anchorAttr attribut DOM stable → clé unique du wrap
63
- * ordinalAttr attribut 0-based pour le calcul de l'intervalle
64
- * null → fallback positionnel (catégories)
53
+ * ordinalAttr attribut 0-based pour le calcul de l'intervalle (null = fallback positionnel)
65
54
  */
66
55
  const KIND = {
67
56
  'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
@@ -72,36 +61,39 @@
72
61
  // ── État global ────────────────────────────────────────────────────────────
73
62
 
74
63
  const S = {
75
- pageKey: null,
76
- cfg: null,
77
- poolsReady: false,
78
- pools: { topics: [], posts: [], categories: [] },
79
- cursors: { topics: 0, posts: 0, categories: 0 },
80
- mountedIds: new Set(),
81
- lastShow: new Map(),
82
- io: null,
83
- domObs: null,
84
- mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
85
- inflight: 0, // showAds() en cours
86
- pending: [], // ids en attente de slot inflight
87
- pendingSet: new Set(),
88
- wrapByKey: new Map(), // anchorKey → wrap DOM node
89
- recycling: new Set(), // ids en cours de séquence destroy→define→displayMore
90
- tcfObs: null, // MutationObserver TCF locator
91
- runQueued: false,
92
- burstActive: false,
64
+ pageKey: null,
65
+ cfg: null,
66
+ poolsReady: false,
67
+ pools: { topics: [], posts: [], categories: [] },
68
+ cursors: { topics: 0, posts: 0, categories: 0 },
69
+ mountedIds: new Set(),
70
+ lastShow: new Map(),
71
+ io: null,
72
+ domObs: null,
73
+ tcfObs: null,
74
+ mutGuard: 0,
75
+ inflight: 0,
76
+ pending: [],
77
+ pendingSet: new Set(),
78
+ wrapByKey: new Map(), // anchorKey wrap DOM node
79
+ recycling: new Set(), // ids en cours de séquence destroy→define→displayMore
80
+ runQueued: false,
81
+ burstActive: false,
93
82
  burstDeadline: 0,
94
- burstCount: 0,
95
- lastBurstTs: 0,
83
+ burstCount: 0,
84
+ lastBurstTs: 0,
96
85
  };
97
86
 
98
- let blockedUntil = 0;
87
+ let blockedUntil = 0;
88
+ let _cfgErrorUntil = 0;
89
+ let _ioMobile = null;
99
90
 
100
- const ts = () => Date.now();
101
- const isBlocked = () => ts() < blockedUntil;
102
- const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
103
- const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
104
- const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
91
+ const ts = () => Date.now();
92
+ const isBlocked = () => ts() < blockedUntil;
93
+ const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
94
+ const _BOOL_TRUE = new Set([true, 'true', 1, '1', 'on']);
95
+ const normBool = v => _BOOL_TRUE.has(v);
96
+ const isFilled = n => !!(n?.querySelector('iframe, ins, img, video, [data-google-container-id]'));
105
97
 
106
98
  function mutate(fn) {
107
99
  S.mutGuard++;
@@ -110,9 +102,6 @@
110
102
 
111
103
  // ── Config ─────────────────────────────────────────────────────────────────
112
104
 
113
- // Fix #2 : backoff 10s sur échec pour éviter de spammer l'API
114
- // Fix #3 : _cfgErrorUntil au scope IIFE pour être réinitialisé dans cleanup()
115
- let _cfgErrorUntil = 0;
116
105
  async function fetchConfig() {
117
106
  if (S.cfg) return S.cfg;
118
107
  if (Date.now() < _cfgErrorUntil) return null;
@@ -180,53 +169,37 @@
180
169
 
181
170
  // ── Wraps — détection ──────────────────────────────────────────────────────
182
171
 
183
- /**
184
- * Vérifie qu'un wrap a encore son ancre dans le DOM.
185
- * Utilisé par adjacentWrap pour ignorer les wraps orphelins.
186
- */
187
172
  function wrapIsLive(wrap) {
188
173
  if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
189
174
  const key = wrap.getAttribute(A_ANCHOR);
190
175
  if (!key) return false;
191
- // Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
192
- // et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
193
176
  if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
194
- // Fallback : registre pas encore à jour ou wrap non enregistré.
195
177
  const colonIdx = key.indexOf(':');
196
178
  const klass = key.slice(0, colonIdx);
197
179
  const anchorId = key.slice(colonIdx + 1);
198
180
  const cfg = KIND[klass];
199
181
  if (!cfg) return false;
200
- // Optimisation : si l'ancre est un frère direct du wrap, pas besoin
201
- // de querySelector global — on cherche parmi les voisins immédiats.
202
182
  const parent = wrap.parentElement;
203
183
  if (parent) {
204
184
  for (const sib of parent.children) {
205
185
  if (sib === wrap) continue;
206
186
  try {
207
- if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
187
+ if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
208
188
  return sib.isConnected;
209
- }
210
189
  } catch (_) {}
211
190
  }
212
191
  }
213
- // Dernier recours : querySelector global
214
192
  try {
215
193
  const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
216
194
  return !!(found?.isConnected);
217
195
  } catch (_) { return false; }
218
196
  }
219
197
 
220
- function adjacentWrap(el) {
221
- return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
222
- }
198
+ const adjacentWrap = el =>
199
+ wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
223
200
 
224
201
  // ── Ancres stables ─────────────────────────────────────────────────────────
225
202
 
226
- /**
227
- * Retourne la valeur de l'attribut stable pour cet élément,
228
- * ou un fallback positionnel si l'attribut est absent.
229
- */
230
203
  function stableId(klass, el) {
231
204
  const attr = KIND[klass]?.anchorAttr;
232
205
  if (attr) {
@@ -245,21 +218,15 @@
245
218
 
246
219
  function findWrap(key) {
247
220
  const w = S.wrapByKey.get(key);
248
- return (w?.isConnected) ? w : null;
221
+ return w?.isConnected ? w : null;
249
222
  }
250
223
 
251
224
  // ── Pool ───────────────────────────────────────────────────────────────────
252
225
 
253
- /**
254
- * Retourne le prochain id disponible dans le pool (round-robin),
255
- * ou null si tous les ids sont montés.
256
- */
257
226
  function pickId(poolKey) {
258
227
  const pool = S.pools[poolKey];
259
228
  if (!pool.length) return null;
260
- // Fix #2 : early-exit si tous les ids sont déjà montés — évite O(n) inutile
261
- if (S.mountedIds.size >= pool.length &&
262
- pool.every(id => S.mountedIds.has(id))) return null;
229
+ if (S.mountedIds.size >= pool.length && pool.every(id => S.mountedIds.has(id))) return null;
263
230
  for (let t = 0; t < pool.length; t++) {
264
231
  const i = S.cursors[poolKey] % pool.length;
265
232
  S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
@@ -271,9 +238,7 @@
271
238
 
272
239
  /**
273
240
  * Pool épuisé : recycle un wrap loin au-dessus du viewport.
274
- * Séquence avec délais (destroyPlaceholders est asynchrone) :
275
- * destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
276
- * displayMore = API Ezoic prévue pour l'infinite scroll.
241
+ * Séquence : destroy([id]) → 300ms → define([id]) 300ms → displayMore([id])
277
242
  * Priorité : wraps vides d'abord, remplis si nécessaire.
278
243
  */
279
244
  function recycleAndMove(klass, targetEl, newKey) {
@@ -282,10 +247,7 @@
282
247
  typeof ez?.define !== 'function' ||
283
248
  typeof ez?.displayMore !== 'function') return null;
284
249
 
285
- const vh = window.innerHeight || 800;
286
- // Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
287
- // après pour neutraliser l'IO — plus de showAds parasite possible.
288
- const threshold = -vh;
250
+ const threshold = -(window.innerHeight || 800);
289
251
  let bestEmpty = null, bestEmptyBottom = Infinity;
290
252
  let bestFilled = null, bestFilledBottom = Infinity;
291
253
 
@@ -305,19 +267,13 @@
305
267
  if (!best) return null;
306
268
  const id = parseInt(best.getAttribute(A_WRAPID), 10);
307
269
  if (!Number.isFinite(id)) return null;
308
- // Fix #1 : éviter de recycler un slot dont la séquence Ezoic est en cours
309
270
  if (S.recycling.has(id)) return null;
310
271
  S.recycling.add(id);
311
272
 
312
273
  const oldKey = best.getAttribute(A_ANCHOR);
313
- // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
314
- // parasite si le nœud était encore dans la zone IO_MARGIN.
315
274
  try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
316
275
  mutate(() => {
317
- best.setAttribute(A_ANCHOR, newKey);
318
- best.setAttribute(A_CREATED, String(ts()));
319
- best.setAttribute(A_SHOWN, '0');
320
- best.classList.remove('is-empty');
276
+ best.setAttribute(A_ANCHOR, newKey);
321
277
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
322
278
  if (ph) ph.innerHTML = '';
323
279
  targetEl.insertAdjacentElement('afterend', best);
@@ -325,16 +281,14 @@
325
281
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
326
282
  S.wrapByKey.set(newKey, best);
327
283
 
328
- // Délais requis : destroyPlaceholders est asynchrone en interne
329
284
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
330
285
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
331
286
  const doDisplay = () => {
332
287
  try { ez.displayMore([id]); } catch (_) {}
333
- S.recycling.delete(id); // Fix #1 : séquence terminée
288
+ S.recycling.delete(id);
334
289
  observePh(id);
335
- try { best.setAttribute(A_SHOWN, String(ts())); } catch (_) {}
336
290
  };
337
- try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
291
+ try { typeof ez.cmd?.push === 'function' ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
338
292
 
339
293
  return { id, wrap: best };
340
294
  }
@@ -344,10 +298,8 @@
344
298
  function makeWrap(id, klass, key) {
345
299
  const w = document.createElement('div');
346
300
  w.className = `${WRAP_CLASS} ${klass}`;
347
- w.setAttribute(A_ANCHOR, key);
348
- w.setAttribute(A_WRAPID, String(id));
349
- w.setAttribute(A_CREATED, String(ts()));
350
- w.setAttribute(A_SHOWN, '0');
301
+ w.setAttribute(A_ANCHOR, key);
302
+ w.setAttribute(A_WRAPID, String(id));
351
303
  w.style.cssText = 'width:100%;display:block;';
352
304
  const ph = document.createElement('div');
353
305
  ph.id = `${PH_PREFIX}${id}`;
@@ -380,13 +332,8 @@
380
332
  } catch (_) {}
381
333
  }
382
334
 
383
-
384
335
  // ── Injection ──────────────────────────────────────────────────────────────
385
336
 
386
- /**
387
- * Ordinal 0-based pour le calcul de l'intervalle d'injection.
388
- * Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
389
- */
390
337
  function ordinal(klass, el) {
391
338
  const attr = KIND[klass]?.ordinalAttr;
392
339
  if (attr) {
@@ -405,18 +352,14 @@
405
352
  function injectBetween(klass, items, interval, showFirst, poolKey) {
406
353
  if (!items.length) return 0;
407
354
  let inserted = 0;
408
-
409
355
  for (const el of items) {
410
356
  if (inserted >= MAX_INSERTS_RUN) break;
411
357
  if (!el?.isConnected) continue;
412
-
413
358
  const ord = ordinal(klass, el);
414
359
  if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
415
360
  if (adjacentWrap(el)) continue;
416
-
417
361
  const key = anchorKey(klass, el);
418
362
  if (findWrap(key)) continue;
419
-
420
363
  const id = pickId(poolKey);
421
364
  if (id) {
422
365
  const w = insertAfter(el, id, klass, key);
@@ -432,13 +375,9 @@
432
375
 
433
376
  // ── IntersectionObserver & Show ────────────────────────────────────────────
434
377
 
435
- // Fix #6 : recréer l'observer si le type d'écran change (rotation, resize).
436
- // isMobile() est évalué à chaque appel pour détecter un changement.
437
- let _ioMobile = null; // dernier état mobile/desktop pour lequel l'IO a été créé
438
378
  function getIO() {
439
379
  const mobile = isMobile();
440
380
  if (S.io && _ioMobile === mobile) return S.io;
441
- // Type d'écran changé ou première création : (re)créer l'observer
442
381
  if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
443
382
  _ioMobile = mobile;
444
383
  try {
@@ -489,19 +428,14 @@
489
428
  drainQueue();
490
429
  };
491
430
  const timer = setTimeout(release, 7000);
492
-
493
431
  requestAnimationFrame(() => {
494
432
  try {
495
433
  if (isBlocked()) { clearTimeout(timer); return release(); }
496
434
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
497
435
  if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
498
-
499
436
  const t = ts();
500
437
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
501
438
  S.lastShow.set(id, t);
502
-
503
- try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
504
-
505
439
  window.ezstandalone = window.ezstandalone || {};
506
440
  const ez = window.ezstandalone;
507
441
  const doShow = () => {
@@ -513,12 +447,10 @@
513
447
  });
514
448
  }
515
449
 
516
-
517
450
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
518
451
  //
519
- // Intercepte ez.showAds() pour :
520
- // ignorer les appels pendant blockedUntil
521
- // – filtrer les ids dont le placeholder n'est pas en DOM
452
+ // Intercepte ez.showAds() pour ignorer les appels pendant blockedUntil
453
+ // et filtrer les ids dont le placeholder n'est pas connecté au DOM.
522
454
 
523
455
  function patchShowAds() {
524
456
  const apply = () => {
@@ -554,36 +486,19 @@
554
486
  async function runCore() {
555
487
  if (isBlocked()) return 0;
556
488
  patchShowAds();
557
-
558
489
  const cfg = await fetchConfig();
559
490
  if (!cfg || cfg.excluded) return 0;
560
491
  initPools(cfg);
561
-
562
492
  const kind = getKind();
563
493
  if (kind === 'other') return 0;
564
-
565
494
  const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
566
495
  if (!normBool(cfgEnable)) return 0;
567
496
  const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
568
497
  return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
569
498
  };
570
-
571
- if (kind === 'topic') return exec(
572
- 'ezoic-ad-message', getPosts,
573
- cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
574
- );
575
-
576
- if (kind === 'categoryTopics') {
577
- return exec(
578
- 'ezoic-ad-between', getTopics,
579
- cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
580
- );
581
- }
582
-
583
- return exec(
584
- 'ezoic-ad-categories', getCategories,
585
- cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
586
- );
499
+ if (kind === 'topic') return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
500
+ if (kind === 'categoryTopics') return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
501
+ return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
587
502
  }
588
503
 
589
504
  // ── Scheduler ──────────────────────────────────────────────────────────────
@@ -607,11 +522,9 @@
607
522
  S.lastBurstTs = t;
608
523
  S.pageKey = pageKey();
609
524
  S.burstDeadline = t + 2000;
610
-
611
525
  if (S.burstActive) return;
612
526
  S.burstActive = true;
613
527
  S.burstCount = 0;
614
-
615
528
  const step = () => {
616
529
  if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
617
530
  S.burstActive = false; return;
@@ -630,25 +543,25 @@
630
543
  function cleanup() {
631
544
  blockedUntil = ts() + 1500;
632
545
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
633
- S.cfg = null;
634
- _cfgErrorUntil = 0; // Fix #3 : réinitialiser le backoff entre les pages
635
- S.poolsReady = false;
636
- S.pools = { topics: [], posts: [], categories: [] };
637
- S.cursors = { topics: 0, posts: 0, categories: 0 };
546
+ S.cfg = null;
547
+ _cfgErrorUntil = 0;
548
+ S.poolsReady = false;
549
+ S.pools = { topics: [], posts: [], categories: [] };
550
+ S.cursors = { topics: 0, posts: 0, categories: 0 };
638
551
  S.mountedIds.clear();
639
552
  S.lastShow.clear();
640
553
  S.wrapByKey.clear();
641
- S.inflight = 0;
642
- S.pending = [];
643
- S.pendingSet.clear();
644
- S.burstActive = false;
645
- S.runQueued = false;
646
554
  S.recycling.clear();
555
+ S.inflight = 0;
556
+ S.pending = [];
557
+ S.pendingSet.clear();
558
+ S.burstActive = false;
559
+ S.runQueued = false;
647
560
  if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
648
561
  if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
649
562
  }
650
563
 
651
- // ── MutationObserver ───────────────────────────────────────────────────────
564
+ // ── MutationObserver DOM ───────────────────────────────────────────────────
652
565
 
653
566
  function ensureDomObserver() {
654
567
  if (S.domObs) return;
@@ -658,9 +571,8 @@
658
571
  for (const m of muts) {
659
572
  for (const n of m.addedNodes) {
660
573
  if (n.nodeType !== 1) continue;
661
- // matches() d'abord (O(1)), querySelector() seulement si nécessaire
662
- if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
663
- allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
574
+ if (allSel.some(s => { try { return n.matches(s); } catch (_) { return false; } }) ||
575
+ allSel.some(s => { try { return !!n.querySelector(s); } catch (_) { return false; } })) {
664
576
  requestBurst(); return;
665
577
  }
666
578
  }
@@ -680,6 +592,7 @@
680
592
  'cannot call refresh on the same page',
681
593
  'no placeholders are currently defined in Refresh',
682
594
  'Debugger iframe already exists',
595
+ '[CMP] Error in custom getTCData',
683
596
  `with id ${PH_PREFIX}`,
684
597
  ];
685
598
  for (const m of ['log', 'info', 'warn', 'error']) {
@@ -702,7 +615,6 @@
702
615
  (document.body || document.documentElement).appendChild(f);
703
616
  };
704
617
  inject();
705
- // Fix #5 : ref stockée dans S pour pouvoir déconnecter au cleanup
706
618
  if (!S.tcfObs) {
707
619
  S.tcfObs = new MutationObserver(inject);
708
620
  S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
@@ -737,25 +649,19 @@
737
649
  function bindNodeBB() {
738
650
  const $ = window.jQuery;
739
651
  if (!$) return;
740
-
741
652
  $(window).off('.nbbEzoic');
742
653
  $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
743
654
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
744
655
  S.pageKey = pageKey();
745
656
  blockedUntil = 0;
746
- // muteConsole et warmNetwork sont idempotents — appelés au boot seulement.
747
- // ensureTcfLocator doit être rappelé : ajaxify retire l'iframe __tcfapiLocator
748
- // du DOM à chaque navigation, le CMP lève une erreur postMessage sans elle.
749
657
  muteConsole(); ensureTcfLocator();
750
658
  patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
751
659
  });
752
-
753
660
  const burstEvts = [
754
- 'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
755
- 'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
661
+ 'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
662
+ 'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded',
756
663
  ].map(e => `${e}.nbbEzoic`).join(' ');
757
664
  $(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
758
-
759
665
  try {
760
666
  require(['hooks'], hooks => {
761
667
  if (typeof hooks?.on !== 'function') return;
@@ -774,11 +680,10 @@
774
680
  ticking = true;
775
681
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
776
682
  }, { passive: true });
777
- // Fix #6 : détecter rotation/resize pour recréer l'IO avec la bonne marge
778
683
  let resizeTimer = 0;
779
684
  window.addEventListener('resize', () => {
780
685
  clearTimeout(resizeTimer);
781
- resizeTimer = setTimeout(() => { getIO(); }, 500);
686
+ resizeTimer = setTimeout(getIO, 500);
782
687
  }, { passive: true });
783
688
  }
784
689