nodebb-plugin-ezoic-infinite 1.7.49 → 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 -206
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.49",
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,50 +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 EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
42
- const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
43
- const MAX_INFLIGHT = 4; // max showAds() simultanés
44
- const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
45
- 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
46
38
 
47
- // Marges IO larges et fixes — observer créé une seule fois au boot
48
39
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
49
40
  const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
50
41
 
@@ -56,13 +47,10 @@
56
47
 
57
48
  /**
58
49
  * Table KIND — source de vérité par kindClass.
59
- *
60
- * sel sélecteur CSS complet des éléments cibles
50
+ * sel sélecteur CSS des éléments cibles
61
51
  * baseTag préfixe tag pour querySelector d'ancre
62
- * (vide pour posts : le sélecteur commence par '[')
63
52
  * anchorAttr attribut DOM stable → clé unique du wrap
64
- * ordinalAttr attribut 0-based pour le calcul de l'intervalle
65
- * null → fallback positionnel (catégories)
53
+ * ordinalAttr attribut 0-based pour le calcul de l'intervalle (null = fallback positionnel)
66
54
  */
67
55
  const KIND = {
68
56
  'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
@@ -73,36 +61,39 @@
73
61
  // ── État global ────────────────────────────────────────────────────────────
74
62
 
75
63
  const S = {
76
- pageKey: null,
77
- cfg: null,
78
- poolsReady: false,
79
- pools: { topics: [], posts: [], categories: [] },
80
- cursors: { topics: 0, posts: 0, categories: 0 },
81
- mountedIds: new Set(),
82
- lastShow: new Map(),
83
- io: null,
84
- domObs: null,
85
- mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
86
- inflight: 0, // showAds() en cours
87
- pending: [], // ids en attente de slot inflight
88
- pendingSet: new Set(),
89
- wrapByKey: new Map(), // anchorKey → wrap DOM node
90
- recycling: new Set(), // ids en cours de séquence destroy→define→displayMore
91
- tcfObs: null, // MutationObserver TCF locator
92
- runQueued: false,
93
- 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,
94
82
  burstDeadline: 0,
95
- burstCount: 0,
96
- lastBurstTs: 0,
83
+ burstCount: 0,
84
+ lastBurstTs: 0,
97
85
  };
98
86
 
99
- let blockedUntil = 0;
87
+ let blockedUntil = 0;
88
+ let _cfgErrorUntil = 0;
89
+ let _ioMobile = null;
100
90
 
101
- const ts = () => Date.now();
102
- const isBlocked = () => ts() < blockedUntil;
103
- const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
104
- const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
105
- 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]'));
106
97
 
107
98
  function mutate(fn) {
108
99
  S.mutGuard++;
@@ -111,9 +102,6 @@
111
102
 
112
103
  // ── Config ─────────────────────────────────────────────────────────────────
113
104
 
114
- // Fix #2 : backoff 10s sur échec pour éviter de spammer l'API
115
- // Fix #3 : _cfgErrorUntil au scope IIFE pour être réinitialisé dans cleanup()
116
- let _cfgErrorUntil = 0;
117
105
  async function fetchConfig() {
118
106
  if (S.cfg) return S.cfg;
119
107
  if (Date.now() < _cfgErrorUntil) return null;
@@ -181,53 +169,37 @@
181
169
 
182
170
  // ── Wraps — détection ──────────────────────────────────────────────────────
183
171
 
184
- /**
185
- * Vérifie qu'un wrap a encore son ancre dans le DOM.
186
- * Utilisé par adjacentWrap pour ignorer les wraps orphelins.
187
- */
188
172
  function wrapIsLive(wrap) {
189
173
  if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
190
174
  const key = wrap.getAttribute(A_ANCHOR);
191
175
  if (!key) return false;
192
- // Lookup O(1) dans le registre — vrai si le wrap EST encore dans le registre
193
- // et connecté au DOM (le registre est tenu à jour par insertAfter/dropWrap).
194
176
  if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
195
- // Fallback : registre pas encore à jour ou wrap non enregistré.
196
177
  const colonIdx = key.indexOf(':');
197
178
  const klass = key.slice(0, colonIdx);
198
179
  const anchorId = key.slice(colonIdx + 1);
199
180
  const cfg = KIND[klass];
200
181
  if (!cfg) return false;
201
- // Optimisation : si l'ancre est un frère direct du wrap, pas besoin
202
- // de querySelector global — on cherche parmi les voisins immédiats.
203
182
  const parent = wrap.parentElement;
204
183
  if (parent) {
205
184
  for (const sib of parent.children) {
206
185
  if (sib === wrap) continue;
207
186
  try {
208
- if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`)) {
187
+ if (sib.matches(`${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`))
209
188
  return sib.isConnected;
210
- }
211
189
  } catch (_) {}
212
190
  }
213
191
  }
214
- // Dernier recours : querySelector global
215
192
  try {
216
193
  const found = document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`);
217
194
  return !!(found?.isConnected);
218
195
  } catch (_) { return false; }
219
196
  }
220
197
 
221
- function adjacentWrap(el) {
222
- return wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
223
- }
198
+ const adjacentWrap = el =>
199
+ wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
224
200
 
225
201
  // ── Ancres stables ─────────────────────────────────────────────────────────
226
202
 
227
- /**
228
- * Retourne la valeur de l'attribut stable pour cet élément,
229
- * ou un fallback positionnel si l'attribut est absent.
230
- */
231
203
  function stableId(klass, el) {
232
204
  const attr = KIND[klass]?.anchorAttr;
233
205
  if (attr) {
@@ -246,21 +218,15 @@
246
218
 
247
219
  function findWrap(key) {
248
220
  const w = S.wrapByKey.get(key);
249
- return (w?.isConnected) ? w : null;
221
+ return w?.isConnected ? w : null;
250
222
  }
251
223
 
252
224
  // ── Pool ───────────────────────────────────────────────────────────────────
253
225
 
254
- /**
255
- * Retourne le prochain id disponible dans le pool (round-robin),
256
- * ou null si tous les ids sont montés.
257
- */
258
226
  function pickId(poolKey) {
259
227
  const pool = S.pools[poolKey];
260
228
  if (!pool.length) return null;
261
- // Fix #2 : early-exit si tous les ids sont déjà montés — évite O(n) inutile
262
- if (S.mountedIds.size >= pool.length &&
263
- 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;
264
230
  for (let t = 0; t < pool.length; t++) {
265
231
  const i = S.cursors[poolKey] % pool.length;
266
232
  S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
@@ -272,9 +238,7 @@
272
238
 
273
239
  /**
274
240
  * Pool épuisé : recycle un wrap loin au-dessus du viewport.
275
- * Séquence avec délais (destroyPlaceholders est asynchrone) :
276
- * destroy([id]) → 300ms → define([id]) → 300ms → displayMore([id])
277
- * displayMore = API Ezoic prévue pour l'infinite scroll.
241
+ * Séquence : destroy([id]) → 300ms → define([id]) 300ms → displayMore([id])
278
242
  * Priorité : wraps vides d'abord, remplis si nécessaire.
279
243
  */
280
244
  function recycleAndMove(klass, targetEl, newKey) {
@@ -283,10 +247,7 @@
283
247
  typeof ez?.define !== 'function' ||
284
248
  typeof ez?.displayMore !== 'function') return null;
285
249
 
286
- const vh = window.innerHeight || 800;
287
- // Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
288
- // après pour neutraliser l'IO — plus de showAds parasite possible.
289
- const threshold = -vh;
250
+ const threshold = -(window.innerHeight || 800);
290
251
  let bestEmpty = null, bestEmptyBottom = Infinity;
291
252
  let bestFilled = null, bestFilledBottom = Infinity;
292
253
 
@@ -306,19 +267,13 @@
306
267
  if (!best) return null;
307
268
  const id = parseInt(best.getAttribute(A_WRAPID), 10);
308
269
  if (!Number.isFinite(id)) return null;
309
- // Fix #1 : éviter de recycler un slot dont la séquence Ezoic est en cours
310
270
  if (S.recycling.has(id)) return null;
311
271
  S.recycling.add(id);
312
272
 
313
273
  const oldKey = best.getAttribute(A_ANCHOR);
314
- // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
315
- // parasite si le nœud était encore dans la zone IO_MARGIN.
316
274
  try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
317
275
  mutate(() => {
318
- best.setAttribute(A_ANCHOR, newKey);
319
- best.setAttribute(A_CREATED, String(ts()));
320
- best.setAttribute(A_SHOWN, '0');
321
- best.classList.remove('is-empty');
276
+ best.setAttribute(A_ANCHOR, newKey);
322
277
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
323
278
  if (ph) ph.innerHTML = '';
324
279
  targetEl.insertAdjacentElement('afterend', best);
@@ -326,20 +281,14 @@
326
281
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
327
282
  S.wrapByKey.set(newKey, best);
328
283
 
329
- // Délais requis : destroyPlaceholders est asynchrone en interne
330
284
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
331
285
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
332
- // Fix #4 : re-observer le ph après displayMore pour déclencher scheduleEmptyCheck
333
- // si la pub ne charge pas (détection wrap vide).
334
286
  const doDisplay = () => {
335
287
  try { ez.displayMore([id]); } catch (_) {}
336
- S.recycling.delete(id); // Fix #1 : séquence terminée
288
+ S.recycling.delete(id);
337
289
  observePh(id);
338
- const t = ts();
339
- try { best.setAttribute(A_SHOWN, String(t)); } catch (_) {}
340
- scheduleEmptyCheck(id, t);
341
290
  };
342
- 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 (_) {}
343
292
 
344
293
  return { id, wrap: best };
345
294
  }
@@ -349,10 +298,8 @@
349
298
  function makeWrap(id, klass, key) {
350
299
  const w = document.createElement('div');
351
300
  w.className = `${WRAP_CLASS} ${klass}`;
352
- w.setAttribute(A_ANCHOR, key);
353
- w.setAttribute(A_WRAPID, String(id));
354
- w.setAttribute(A_CREATED, String(ts()));
355
- w.setAttribute(A_SHOWN, '0');
301
+ w.setAttribute(A_ANCHOR, key);
302
+ w.setAttribute(A_WRAPID, String(id));
356
303
  w.style.cssText = 'width:100%;display:block;';
357
304
  const ph = document.createElement('div');
358
305
  ph.id = `${PH_PREFIX}${id}`;
@@ -385,13 +332,8 @@
385
332
  } catch (_) {}
386
333
  }
387
334
 
388
-
389
335
  // ── Injection ──────────────────────────────────────────────────────────────
390
336
 
391
- /**
392
- * Ordinal 0-based pour le calcul de l'intervalle d'injection.
393
- * Utilise ordinalAttr si défini, sinon compte les frères dans le parent.
394
- */
395
337
  function ordinal(klass, el) {
396
338
  const attr = KIND[klass]?.ordinalAttr;
397
339
  if (attr) {
@@ -410,18 +352,14 @@
410
352
  function injectBetween(klass, items, interval, showFirst, poolKey) {
411
353
  if (!items.length) return 0;
412
354
  let inserted = 0;
413
-
414
355
  for (const el of items) {
415
356
  if (inserted >= MAX_INSERTS_RUN) break;
416
357
  if (!el?.isConnected) continue;
417
-
418
358
  const ord = ordinal(klass, el);
419
359
  if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
420
360
  if (adjacentWrap(el)) continue;
421
-
422
361
  const key = anchorKey(klass, el);
423
362
  if (findWrap(key)) continue;
424
-
425
363
  const id = pickId(poolKey);
426
364
  if (id) {
427
365
  const w = insertAfter(el, id, klass, key);
@@ -437,13 +375,9 @@
437
375
 
438
376
  // ── IntersectionObserver & Show ────────────────────────────────────────────
439
377
 
440
- // Fix #6 : recréer l'observer si le type d'écran change (rotation, resize).
441
- // isMobile() est évalué à chaque appel pour détecter un changement.
442
- let _ioMobile = null; // dernier état mobile/desktop pour lequel l'IO a été créé
443
378
  function getIO() {
444
379
  const mobile = isMobile();
445
380
  if (S.io && _ioMobile === mobile) return S.io;
446
- // Type d'écran changé ou première création : (re)créer l'observer
447
381
  if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
448
382
  _ioMobile = mobile;
449
383
  try {
@@ -494,24 +428,18 @@
494
428
  drainQueue();
495
429
  };
496
430
  const timer = setTimeout(release, 7000);
497
-
498
431
  requestAnimationFrame(() => {
499
432
  try {
500
433
  if (isBlocked()) { clearTimeout(timer); return release(); }
501
434
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
502
435
  if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
503
-
504
436
  const t = ts();
505
437
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
506
438
  S.lastShow.set(id, t);
507
-
508
- try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
509
-
510
439
  window.ezstandalone = window.ezstandalone || {};
511
440
  const ez = window.ezstandalone;
512
441
  const doShow = () => {
513
442
  try { ez.showAds(id); } catch (_) {}
514
- scheduleEmptyCheck(id, t);
515
443
  setTimeout(() => { clearTimeout(timer); release(); }, 700);
516
444
  };
517
445
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
@@ -519,26 +447,10 @@
519
447
  });
520
448
  }
521
449
 
522
- function scheduleEmptyCheck(id, showTs) {
523
- setTimeout(() => {
524
- try {
525
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
526
- const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
527
- if (!wrap || !ph?.isConnected) return;
528
- if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
529
- // is-empty désactivé pour ezoic-ad-between : Ezoic peut prendre >20s
530
- // à charger sur liste de catégorie — collapse prématuré des wraps.
531
- if (wrap.classList.contains('ezoic-ad-between')) return;
532
- wrap.classList.toggle('is-empty', !isFilled(ph));
533
- } catch (_) {}
534
- }, EMPTY_CHECK_MS);
535
- }
536
-
537
450
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
538
451
  //
539
- // Intercepte ez.showAds() pour :
540
- // ignorer les appels pendant blockedUntil
541
- // – 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.
542
454
 
543
455
  function patchShowAds() {
544
456
  const apply = () => {
@@ -574,36 +486,19 @@
574
486
  async function runCore() {
575
487
  if (isBlocked()) return 0;
576
488
  patchShowAds();
577
-
578
489
  const cfg = await fetchConfig();
579
490
  if (!cfg || cfg.excluded) return 0;
580
491
  initPools(cfg);
581
-
582
492
  const kind = getKind();
583
493
  if (kind === 'other') return 0;
584
-
585
494
  const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
586
495
  if (!normBool(cfgEnable)) return 0;
587
496
  const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
588
497
  return injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
589
498
  };
590
-
591
- if (kind === 'topic') return exec(
592
- 'ezoic-ad-message', getPosts,
593
- cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
594
- );
595
-
596
- if (kind === 'categoryTopics') {
597
- return exec(
598
- 'ezoic-ad-between', getTopics,
599
- cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
600
- );
601
- }
602
-
603
- return exec(
604
- 'ezoic-ad-categories', getCategories,
605
- cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
606
- );
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');
607
502
  }
608
503
 
609
504
  // ── Scheduler ──────────────────────────────────────────────────────────────
@@ -627,11 +522,9 @@
627
522
  S.lastBurstTs = t;
628
523
  S.pageKey = pageKey();
629
524
  S.burstDeadline = t + 2000;
630
-
631
525
  if (S.burstActive) return;
632
526
  S.burstActive = true;
633
527
  S.burstCount = 0;
634
-
635
528
  const step = () => {
636
529
  if (pageKey() !== S.pageKey || isBlocked() || ts() > S.burstDeadline || S.burstCount >= 8) {
637
530
  S.burstActive = false; return;
@@ -650,25 +543,25 @@
650
543
  function cleanup() {
651
544
  blockedUntil = ts() + 1500;
652
545
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
653
- S.cfg = null;
654
- _cfgErrorUntil = 0; // Fix #3 : réinitialiser le backoff entre les pages
655
- S.poolsReady = false;
656
- S.pools = { topics: [], posts: [], categories: [] };
657
- 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 };
658
551
  S.mountedIds.clear();
659
552
  S.lastShow.clear();
660
553
  S.wrapByKey.clear();
661
- S.inflight = 0;
662
- S.pending = [];
663
- S.pendingSet.clear();
664
- S.burstActive = false;
665
- S.runQueued = false;
666
554
  S.recycling.clear();
555
+ S.inflight = 0;
556
+ S.pending = [];
557
+ S.pendingSet.clear();
558
+ S.burstActive = false;
559
+ S.runQueued = false;
667
560
  if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
668
561
  if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
669
562
  }
670
563
 
671
- // ── MutationObserver ───────────────────────────────────────────────────────
564
+ // ── MutationObserver DOM ───────────────────────────────────────────────────
672
565
 
673
566
  function ensureDomObserver() {
674
567
  if (S.domObs) return;
@@ -678,9 +571,8 @@
678
571
  for (const m of muts) {
679
572
  for (const n of m.addedNodes) {
680
573
  if (n.nodeType !== 1) continue;
681
- // matches() d'abord (O(1)), querySelector() seulement si nécessaire
682
- if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
683
- 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; } })) {
684
576
  requestBurst(); return;
685
577
  }
686
578
  }
@@ -700,6 +592,7 @@
700
592
  'cannot call refresh on the same page',
701
593
  'no placeholders are currently defined in Refresh',
702
594
  'Debugger iframe already exists',
595
+ '[CMP] Error in custom getTCData',
703
596
  `with id ${PH_PREFIX}`,
704
597
  ];
705
598
  for (const m of ['log', 'info', 'warn', 'error']) {
@@ -722,7 +615,6 @@
722
615
  (document.body || document.documentElement).appendChild(f);
723
616
  };
724
617
  inject();
725
- // Fix #5 : ref stockée dans S pour pouvoir déconnecter au cleanup
726
618
  if (!S.tcfObs) {
727
619
  S.tcfObs = new MutationObserver(inject);
728
620
  S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
@@ -757,25 +649,19 @@
757
649
  function bindNodeBB() {
758
650
  const $ = window.jQuery;
759
651
  if (!$) return;
760
-
761
652
  $(window).off('.nbbEzoic');
762
653
  $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
763
654
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
764
655
  S.pageKey = pageKey();
765
656
  blockedUntil = 0;
766
- // muteConsole et warmNetwork sont idempotents — appelés au boot seulement.
767
- // ensureTcfLocator doit être rappelé : ajaxify retire l'iframe __tcfapiLocator
768
- // du DOM à chaque navigation, le CMP lève une erreur postMessage sans elle.
769
657
  muteConsole(); ensureTcfLocator();
770
658
  patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
771
659
  });
772
-
773
660
  const burstEvts = [
774
- 'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded',
775
- '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',
776
663
  ].map(e => `${e}.nbbEzoic`).join(' ');
777
664
  $(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
778
-
779
665
  try {
780
666
  require(['hooks'], hooks => {
781
667
  if (typeof hooks?.on !== 'function') return;
@@ -794,11 +680,10 @@
794
680
  ticking = true;
795
681
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
796
682
  }, { passive: true });
797
- // Fix #6 : détecter rotation/resize pour recréer l'IO avec la bonne marge
798
683
  let resizeTimer = 0;
799
684
  window.addEventListener('resize', () => {
800
685
  clearTimeout(resizeTimer);
801
- resizeTimer = setTimeout(() => { getIO(); }, 500);
686
+ resizeTimer = setTimeout(getIO, 500);
802
687
  }, { passive: true });
803
688
  }
804
689