nodebb-plugin-ezoic-infinite 1.7.90 → 1.7.92

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.90",
3
+ "version": "1.7.92",
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,7 +77,6 @@
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
- const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
81
80
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
82
81
  const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
83
82
  const MAX_INFLIGHT = 4; // max showAds() simultanés
@@ -127,6 +126,7 @@
127
126
  pending: [], // ids en attente de slot inflight
128
127
  pendingSet: new Set(),
129
128
  wrapByKey: new Map(), // anchorKey → wrap DOM node
129
+ fillObsById: new Map(), // id -> MutationObserver
130
130
  runQueued: false,
131
131
  burstActive: false,
132
132
  burstDeadline: 0,
@@ -144,6 +144,33 @@
144
144
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
145
145
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
146
146
 
147
+
148
+ function unwatchPlaceholderFillById(id) {
149
+ const obs = S.fillObsById.get(id);
150
+ if (obs) { try { obs.disconnect(); } catch (_) {} S.fillObsById.delete(id); }
151
+ }
152
+
153
+ function uncollapseIfFilled(ph) {
154
+ try {
155
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
156
+ if (!wrap) return false;
157
+ if (!isFilled(ph)) return false;
158
+ wrap.classList.remove('is-empty');
159
+ return true;
160
+ } catch (_) { return false; }
161
+ }
162
+
163
+ function watchPlaceholderFill(id) {
164
+ const ph = document.getElementById(`${PH_PREFIX}${id}`);
165
+ if (!ph?.isConnected || S.fillObsById.has(id)) return;
166
+ try {
167
+ const obs = new MutationObserver(() => { try { uncollapseIfFilled(ph); } catch (_) {} });
168
+ obs.observe(ph, { childList: true, subtree: true, attributes: true });
169
+ S.fillObsById.set(id, obs);
170
+ uncollapseIfFilled(ph);
171
+ } catch (_) {}
172
+ }
173
+
147
174
  function mutate(fn) {
148
175
  S.mutGuard++;
149
176
  try { fn(); } finally { S.mutGuard--; }
@@ -284,6 +311,26 @@
284
311
  return (w?.isConnected) ? w : null;
285
312
  }
286
313
 
314
+ function sweepDeadWraps() {
315
+ let freed = 0;
316
+ for (const [key, wrap] of Array.from(S.wrapByKey.entries())) {
317
+ if (wrap?.isConnected) continue;
318
+ const id = parseInt(wrap?.getAttribute?.(A_WRAPID) || '0', 10);
319
+ if (Number.isFinite(id) && id > 0) {
320
+ S.mountedIds.delete(id);
321
+ unwatchPlaceholderFillById(id);
322
+ S.lastShow.delete(id);
323
+ S.pendingSet.delete(id);
324
+ }
325
+ S.wrapByKey.delete(key);
326
+ freed++;
327
+ }
328
+ if (freed && S.pending.length) {
329
+ S.pending = S.pending.filter(id => !S.mountedIds.has(id));
330
+ }
331
+ return freed;
332
+ }
333
+
287
334
  // ── Pool ───────────────────────────────────────────────────────────────────
288
335
 
289
336
  /**
@@ -330,9 +377,10 @@
330
377
  let belowEmpty = null, belowEmptyTop = -Infinity;
331
378
  let belowFilled = null, belowFilledTop = -Infinity;
332
379
 
333
- document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
380
+ for (const wrap of S.wrapByKey.values()) {
381
+ if (!wrap?.classList?.contains?.(klass)) continue;
334
382
  try {
335
- if (!wrap?.isConnected) return;
383
+ if (!wrap?.isConnected) continue;
336
384
  const rect = wrap.getBoundingClientRect();
337
385
 
338
386
  if (rect.bottom < aboveThreshold) {
@@ -341,7 +389,7 @@
341
389
  } else {
342
390
  if (rect.bottom < aboveFilledBottom) { aboveFilledBottom = rect.bottom; aboveFilled = wrap; }
343
391
  }
344
- return;
392
+ continue;
345
393
  }
346
394
 
347
395
  if (rect.top > belowThreshold) {
@@ -352,7 +400,7 @@
352
400
  }
353
401
  }
354
402
  } catch (_) {}
355
- });
403
+ }
356
404
 
357
405
  const preferBelow = (S.scrollDir < 0) || (targetRect.top < vh * 0.5);
358
406
  const pickAbove = () => aboveEmpty ?? aboveFilled;
@@ -413,6 +461,7 @@
413
461
  mutate(() => el.insertAdjacentElement('afterend', w));
414
462
  S.mountedIds.add(id);
415
463
  S.wrapByKey.set(key, w);
464
+ watchPlaceholderFill(id);
416
465
  return w;
417
466
  }
418
467
 
@@ -421,7 +470,7 @@
421
470
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
422
471
  if (ph instanceof Element) S.io?.unobserve(ph);
423
472
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
424
- if (Number.isFinite(id)) S.mountedIds.delete(id);
473
+ if (Number.isFinite(id)) { S.mountedIds.delete(id); unwatchPlaceholderFillById(id); S.lastShow.delete(id); S.pendingSet.delete(id); }
425
474
  const key = w.getAttribute(A_ANCHOR);
426
475
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
427
476
  w.remove();
@@ -496,7 +545,8 @@
496
545
  const key = anchorKey(klass, el);
497
546
  if (findWrap(key)) continue;
498
547
 
499
- const id = pickId(poolKey);
548
+ let id = pickId(poolKey);
549
+ if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
500
550
  if (id) {
501
551
  const w = insertAfter(el, id, klass, key);
502
552
  if (w) { observePh(id); inserted++; }
@@ -528,7 +578,9 @@
528
578
 
529
579
  function observePh(id) {
530
580
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
531
- if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
581
+ if (!ph?.isConnected) return;
582
+ watchPlaceholderFill(id);
583
+ try { getIO()?.observe(ph); } catch (_) {}
532
584
  }
533
585
 
534
586
  function enqueueShow(id) {
@@ -568,6 +620,8 @@
568
620
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
569
621
  if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
570
622
 
623
+ try { ph.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
624
+
571
625
  const t = ts();
572
626
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
573
627
  S.lastShow.set(id, t);
@@ -578,7 +632,6 @@
578
632
  const ez = window.ezstandalone;
579
633
  const doShow = () => {
580
634
  try { ez.showAds(id); } catch (_) {}
581
- scheduleEmptyCheck(id, t);
582
635
  setTimeout(() => { clearTimeout(timer); release(); }, 700);
583
636
  };
584
637
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
@@ -586,17 +639,6 @@
586
639
  });
587
640
  }
588
641
 
589
- function scheduleEmptyCheck(id, showTs) {
590
- setTimeout(() => {
591
- try {
592
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
593
- const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
594
- if (!wrap || !ph?.isConnected) return;
595
- if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
596
- wrap.classList.toggle('is-empty', !isFilled(ph));
597
- } catch (_) {}
598
- }, EMPTY_CHECK_MS);
599
- }
600
642
 
601
643
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
602
644
  //
@@ -638,6 +680,7 @@
638
680
  async function runCore() {
639
681
  if (isBlocked()) return 0;
640
682
  patchShowAds();
683
+ sweepDeadWraps();
641
684
 
642
685
  const cfg = await fetchConfig();
643
686
  if (!cfg || cfg.excluded) return 0;
@@ -722,6 +765,8 @@
722
765
  S.mountedIds.clear();
723
766
  S.lastShow.clear();
724
767
  S.wrapByKey.clear();
768
+ for (const obs of S.fillObsById.values()) { try { obs.disconnect(); } catch (_) {} }
769
+ S.fillObsById.clear();
725
770
  S.inflight = 0;
726
771
  S.pending = [];
727
772
  S.pendingSet.clear();
@@ -737,6 +782,15 @@
737
782
  S.domObs = new MutationObserver(muts => {
738
783
  if (S.mutGuard > 0 || isBlocked()) return;
739
784
  for (const m of muts) {
785
+ let sawWrapRemoval = false;
786
+ for (const n of m.removedNodes || []) {
787
+ if (n.nodeType !== 1) continue;
788
+ try {
789
+ if (n.matches?.(`.${WRAP_CLASS}`) || n.querySelector?.(`.${WRAP_CLASS}`)) { sawWrapRemoval = true; }
790
+ } catch (_) {}
791
+ }
792
+ if (sawWrapRemoval) sweepDeadWraps();
793
+
740
794
  for (const n of m.addedNodes) {
741
795
  if (n.nodeType !== 1) continue;
742
796
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
package/public/style.css CHANGED
@@ -56,20 +56,6 @@
56
56
  top: auto !important;
57
57
  }
58
58
 
59
- /* ── État vide ────────────────────────────────────────────────────────────── */
60
- /*
61
- Ajouté 20s après showAds si aucun fill détecté.
62
- Collapse à 1px (pas 0) : reste observable par l'IO si le fill arrive tard.
63
- */
64
- .nodebb-ezoic-wrap.is-empty {
65
- display: block !important;
66
- height: 1px !important;
67
- min-height: 1px !important;
68
- max-height: 1px !important;
69
- margin: 0 !important;
70
- padding: 0 !important;
71
- overflow: hidden !important;
72
- }
73
59
 
74
60
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
75
61
  .ezoic-ad {