nodebb-plugin-ezoic-infinite 1.7.87 → 1.7.89

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.87",
3
+ "version": "1.7.89",
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
@@ -77,9 +77,7 @@
77
77
  const A_CREATED = 'data-ezoic-created'; // timestamp création ms
78
78
  const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
79
79
 
80
- // anti-race fill async (Ezoic peut remplir bien après showAds)
81
- const EMPTY_CHECK_PASSES = [20_000, 25_000, 35_000];
82
-
80
+ const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
83
81
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
84
82
  const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
85
83
  const MAX_INFLIGHT = 4; // max showAds() simultanés
@@ -122,8 +120,6 @@
122
120
  cursors: { topics: 0, posts: 0, categories: 0 },
123
121
  mountedIds: new Set(),
124
122
  lastShow: new Map(),
125
- emptyChecks: new Map(), // id -> [timerIds] checks is-empty multi-pass
126
- fillObs: new Map(), // id -> MutationObserver placeholder fill tardif
127
123
  io: null,
128
124
  domObs: null,
129
125
  mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
@@ -146,48 +142,6 @@
146
142
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
147
143
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
148
144
 
149
- function clearEmptyChecks(id) {
150
- const arr = S.emptyChecks.get(id);
151
- if (arr) {
152
- for (const t of arr) clearTimeout(t);
153
- S.emptyChecks.delete(id);
154
- }
155
- }
156
-
157
- function queueEmptyCheck(id, timerId) {
158
- const arr = S.emptyChecks.get(id) || [];
159
- arr.push(timerId);
160
- S.emptyChecks.set(id, arr);
161
- }
162
-
163
- function uncollapseIfFilled(ph) {
164
- try {
165
- const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
166
- if (!wrap) return;
167
- if (isFilled(ph)) wrap.classList.remove('is-empty');
168
- } catch (_) {}
169
- }
170
-
171
- function watchPlaceholderFill(id) {
172
- try {
173
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
174
- if (!ph?.isConnected) return;
175
- if (S.fillObs.has(id)) return;
176
- const obs = new MutationObserver(() => uncollapseIfFilled(ph));
177
- obs.observe(ph, { childList: true, subtree: true, attributes: true });
178
- S.fillObs.set(id, obs);
179
- uncollapseIfFilled(ph);
180
- } catch (_) {}
181
- }
182
-
183
- function unwatchPlaceholderFill(id) {
184
- const obs = S.fillObs.get(id);
185
- if (obs) {
186
- try { obs.disconnect(); } catch (_) {}
187
- S.fillObs.delete(id);
188
- }
189
- }
190
-
191
145
  function mutate(fn) {
192
146
  S.mutGuard++;
193
147
  try { fn(); } finally { S.mutGuard--; }
@@ -328,6 +282,28 @@
328
282
  return (w?.isConnected) ? w : null;
329
283
  }
330
284
 
285
+ /**
286
+ * Libère les ids de wraps supprimés du DOM (virtualisation NodeBB / rerender).
287
+ * Sans ce sweep, mountedIds peut conserver des ids fantômes → pool épuisé
288
+ * après long scroll alors qu'aucun wrap recyclable n'existe encore en DOM.
289
+ */
290
+ function sweepDeadWraps() {
291
+ for (const [key, w] of S.wrapByKey.entries()) {
292
+ if (w?.isConnected) continue;
293
+ const id = parseInt(w?.getAttribute?.(A_WRAPID), 10);
294
+ if (Number.isFinite(id)) {
295
+ S.mountedIds.delete(id);
296
+ S.lastShow.delete(id);
297
+ S.pendingSet.delete(id);
298
+ }
299
+ S.wrapByKey.delete(key);
300
+ }
301
+ if (S.pending.length) {
302
+ S.pending = S.pending.filter(id => !S.pendingSet.has(id) || document.getElementById(`${PH_PREFIX}${id}`)?.isConnected);
303
+ S.pendingSet = new Set(S.pending);
304
+ }
305
+ }
306
+
331
307
  // ── Pool ───────────────────────────────────────────────────────────────────
332
308
 
333
309
  /**
@@ -387,7 +363,6 @@
387
363
  // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
388
364
  // parasite si le nœud était encore dans la zone IO_MARGIN.
389
365
  try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
390
- if (Number.isFinite(id)) clearEmptyChecks(id);
391
366
  mutate(() => {
392
367
  best.setAttribute(A_ANCHOR, newKey);
393
368
  best.setAttribute(A_CREATED, String(ts()));
@@ -399,7 +374,6 @@
399
374
  });
400
375
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
401
376
  S.wrapByKey.set(newKey, best);
402
- observePh(id);
403
377
 
404
378
  // Délais requis : destroyPlaceholders est asynchrone en interne
405
379
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
@@ -444,7 +418,6 @@
444
418
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
445
419
  if (ph instanceof Element) S.io?.unobserve(ph);
446
420
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
447
- if (Number.isFinite(id)) { clearEmptyChecks(id); unwatchPlaceholderFill(id); }
448
421
  if (Number.isFinite(id)) S.mountedIds.delete(id);
449
422
  const key = w.getAttribute(A_ANCHOR);
450
423
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
@@ -520,7 +493,13 @@
520
493
  const key = anchorKey(klass, el);
521
494
  if (findWrap(key)) continue;
522
495
 
523
- const id = pickId(poolKey);
496
+ let id = pickId(poolKey);
497
+ if (!id) {
498
+ // Réessaie après sweep : des wraps ont pu être retirés du DOM (virtualisation)
499
+ // sans passer par dropWrap, laissant des ids fantômes dans mountedIds.
500
+ sweepDeadWraps();
501
+ id = pickId(poolKey);
502
+ }
524
503
  if (id) {
525
504
  const w = insertAfter(el, id, klass, key);
526
505
  if (w) { observePh(id); inserted++; }
@@ -552,10 +531,7 @@
552
531
 
553
532
  function observePh(id) {
554
533
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
555
- if (ph?.isConnected) {
556
- try { getIO()?.observe(ph); } catch (_) {}
557
- watchPlaceholderFill(id);
558
- }
534
+ if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
559
535
  }
560
536
 
561
537
  function enqueueShow(id) {
@@ -595,9 +571,6 @@
595
571
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
596
572
  if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
597
573
 
598
- clearEmptyChecks(id);
599
- try { ph.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
600
-
601
574
  const t = ts();
602
575
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
603
576
  S.lastShow.set(id, t);
@@ -617,24 +590,15 @@
617
590
  }
618
591
 
619
592
  function scheduleEmptyCheck(id, showTs) {
620
- clearEmptyChecks(id);
621
-
622
- const runCheck = () => {
593
+ setTimeout(() => {
623
594
  try {
624
595
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
625
596
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
626
597
  if (!wrap || !ph?.isConnected) return;
627
598
  if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
628
- const filled = isFilled(ph);
629
- if (filled) wrap.classList.remove('is-empty');
630
- else wrap.classList.add('is-empty');
599
+ wrap.classList.toggle('is-empty', !isFilled(ph));
631
600
  } catch (_) {}
632
- };
633
-
634
- for (const delay of EMPTY_CHECK_PASSES) {
635
- const tid = setTimeout(runCheck, delay);
636
- queueEmptyCheck(id, tid);
637
- }
601
+ }, EMPTY_CHECK_MS);
638
602
  }
639
603
 
640
604
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
@@ -677,6 +641,7 @@
677
641
  async function runCore() {
678
642
  if (isBlocked()) return 0;
679
643
  patchShowAds();
644
+ sweepDeadWraps();
680
645
 
681
646
  const cfg = await fetchConfig();
682
647
  if (!cfg || cfg.excluded) return 0;
@@ -764,10 +729,6 @@
764
729
  S.inflight = 0;
765
730
  S.pending = [];
766
731
  S.pendingSet.clear();
767
- S.emptyChecks.forEach(arr => { try { arr.forEach(clearTimeout); } catch (_) {} });
768
- S.emptyChecks.clear();
769
- S.fillObs.forEach(obs => { try { obs.disconnect(); } catch (_) {} });
770
- S.fillObs.clear();
771
732
  S.burstActive = false;
772
733
  S.runQueued = false;
773
734
  }
@@ -779,16 +740,26 @@
779
740
  const allSel = [SEL.post, SEL.topic, SEL.category];
780
741
  S.domObs = new MutationObserver(muts => {
781
742
  if (S.mutGuard > 0 || isBlocked()) return;
743
+ let sawRelevantAdd = false;
744
+ let sawWrapRemoval = false;
782
745
  for (const m of muts) {
746
+ for (const n of m.removedNodes) {
747
+ if (n.nodeType !== 1) continue;
748
+ if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || n.querySelector?.(`.${WRAP_CLASS}`)) {
749
+ sawWrapRemoval = true;
750
+ }
751
+ }
783
752
  for (const n of m.addedNodes) {
784
753
  if (n.nodeType !== 1) continue;
785
754
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
786
755
  if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
787
756
  allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
788
- requestBurst(); return;
757
+ sawRelevantAdd = true;
789
758
  }
790
759
  }
791
760
  }
761
+ if (sawWrapRemoval) sweepDeadWraps();
762
+ if (sawRelevantAdd) requestBurst();
792
763
  });
793
764
  try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
794
765
  }
package/public/style.css CHANGED
@@ -71,14 +71,6 @@
71
71
  overflow: hidden !important;
72
72
  }
73
73
 
74
- /* Filet de sécurité : si un fill est présent malgré is-empty, on ne collapse pas */
75
- .nodebb-ezoic-wrap.is-empty:has(iframe, ins, img, video, [data-google-container-id]) {
76
- height: auto !important;
77
- min-height: 1px !important;
78
- max-height: none !important;
79
- overflow: visible !important;
80
- }
81
-
82
74
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
83
75
  .ezoic-ad {
84
76
  margin: 0 !important;