nodebb-plugin-ezoic-infinite 1.7.99 → 1.8.1

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.99",
3
+ "version": "1.8.1",
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,17 +77,20 @@
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
80
+ // Tunables (stables en prod)
81
81
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
82
- const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
83
- const MAX_INFLIGHT = 4; // max showAds() simultanés
82
+ const MAX_INSERTS_RUN = 10; // plus réactif si NodeBB injecte en rafale
83
+ const MAX_INFLIGHT = 2; // max showAds() simultanés (garde-fou)
84
84
  const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
85
- const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
86
- const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
85
+ const SHOW_THROTTLE_MS = 500; // anti-spam showAds() par id (plus réactif)
86
+ const SHOW_RELEASE_MS = 300; // relâche inflight après showAds() batché
87
+ const SHOW_FAILSAFE_MS = 7000; // relâche forcée si stack pub lente
88
+ const BATCH_FLUSH_MS = 30; // micro-buffer pour regrouper les ids proches
89
+ const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
87
90
 
88
91
  // Marges IO larges et fixes — observer créé une seule fois au boot
89
- const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
90
- const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
92
+ const IO_MARGIN_DESKTOP = '3000px 0px 3000px 0px';
93
+ const IO_MARGIN_MOBILE = '2000px 0px 2000px 0px';
91
94
 
92
95
  const SEL = {
93
96
  post: '[component="post"][data-pid]',
@@ -127,6 +130,8 @@
127
130
  inflight: 0, // showAds() en cours
128
131
  pending: [], // ids en attente de slot inflight
129
132
  pendingSet: new Set(),
133
+ showBatchTimer: 0,
134
+ sweepQueued: false,
130
135
  wrapByKey: new Map(), // anchorKey → wrap DOM node
131
136
  ezActiveIds: new Set(), // ids déjà passés à showAds/displayMore
132
137
  scrollDir: 1, // 1=bas, -1=haut
@@ -148,6 +153,33 @@
148
153
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
149
154
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
150
155
 
156
+ function phEl(id) {
157
+ return document.getElementById(`${PH_PREFIX}${id}`);
158
+ }
159
+
160
+ function hasSinglePlaceholder(id) {
161
+ try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
162
+ }
163
+
164
+ function canShowPlaceholderId(id, now = ts()) {
165
+ const n = parseInt(id, 10);
166
+ if (!Number.isFinite(n) || n <= 0) return false;
167
+ if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
168
+ const ph = phEl(n);
169
+ if (!ph?.isConnected || isFilled(ph)) return false;
170
+ if (!hasSinglePlaceholder(n)) return false;
171
+ return true;
172
+ }
173
+
174
+ function queueSweepDeadWraps() {
175
+ if (S.sweepQueued) return;
176
+ S.sweepQueued = true;
177
+ requestAnimationFrame(() => {
178
+ S.sweepQueued = false;
179
+ sweepDeadWraps();
180
+ });
181
+ }
182
+
151
183
  function getDynamicShowBatchMax() {
152
184
  const speed = S.scrollSpeed || 0;
153
185
  const pend = S.pending.length;
@@ -330,6 +362,25 @@ function destroyEzoicId(id) {
330
362
  return null;
331
363
  }
332
364
 
365
+ function sweepDeadWraps() {
366
+ // NodeBB peut retirer des nœuds sans passer par dropWrap() (virtualisation / rerender).
367
+ // On libère alors les IDs/keys fantômes pour éviter l'épuisement du pool.
368
+ for (const [key, wrap] of Array.from(S.wrapByKey.entries())) {
369
+ if (wrap?.isConnected) continue;
370
+ S.wrapByKey.delete(key);
371
+ const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
372
+ if (Number.isFinite(id)) {
373
+ S.mountedIds.delete(id);
374
+ S.pendingSet.delete(id);
375
+ S.lastShow.delete(id);
376
+ S.ezActiveIds.delete(id);
377
+ }
378
+ }
379
+ if (S.pending.length) {
380
+ S.pending = S.pending.filter(id => S.pendingSet.has(id));
381
+ }
382
+ }
383
+
333
384
  /**
334
385
  * Pool épuisé : recycle un wrap loin au-dessus du viewport.
335
386
  * Séquence avec délais (destroyPlaceholders est asynchrone) :
@@ -353,13 +404,14 @@ function recycleAndMove(klass, targetEl, newKey) {
353
404
  let bestAnyEmpty = null, bestAnyMetric = Infinity;
354
405
  let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
355
406
 
356
- document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
407
+ for (const wrap of S.wrapByKey.values()) {
408
+ if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
357
409
  try {
358
410
  const rect = wrap.getBoundingClientRect();
359
411
  const isAbove = rect.bottom <= farAbove;
360
412
  const isBelow = rect.top >= farBelow;
361
413
  const anyFar = isAbove || isBelow;
362
- if (!anyFar) return;
414
+ if (!anyFar) continue;
363
415
 
364
416
  const qualifies = preferAbove ? isAbove : isBelow;
365
417
  const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
@@ -378,7 +430,7 @@ function recycleAndMove(klass, targetEl, newKey) {
378
430
  if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
379
431
  }
380
432
  } catch (_) {}
381
- });
433
+ }
382
434
 
383
435
  const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
384
436
  if (!best) return null;
@@ -523,7 +575,8 @@ function recycleAndMove(klass, targetEl, newKey) {
523
575
  const key = anchorKey(klass, el);
524
576
  if (findWrap(key)) continue;
525
577
 
526
- const id = pickId(poolKey);
578
+ let id = pickId(poolKey);
579
+ if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
527
580
  if (id) {
528
581
  const w = insertAfter(el, id, klass, key);
529
582
  if (w) { observePh(id); inserted++; }
@@ -554,15 +607,34 @@ function recycleAndMove(klass, targetEl, newKey) {
554
607
  }
555
608
 
556
609
  function observePh(id) {
557
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
610
+ const ph = phEl(id);
558
611
  if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
612
+ // Fast-path : si déjà proche viewport, ne pas attendre un callback IO complet
613
+ try {
614
+ if (!ph?.isConnected) return;
615
+ const rect = ph.getBoundingClientRect();
616
+ const vh = window.innerHeight || 800;
617
+ const preload = isMobile() ? 1400 : 1000;
618
+ if (rect.top <= vh + preload && rect.bottom >= -preload) enqueueShow(id);
619
+ } catch (_) {}
559
620
  }
560
621
 
561
622
  function enqueueShow(id) {
562
623
  if (!id || isBlocked()) return;
563
- if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
564
- if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
565
- drainQueue();
624
+ const n = parseInt(id, 10);
625
+ if (!Number.isFinite(n) || n <= 0) return;
626
+ if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
627
+ if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
628
+ scheduleDrainQueue();
629
+ }
630
+
631
+ function scheduleDrainQueue() {
632
+ if (isBlocked()) return;
633
+ if (S.showBatchTimer) return;
634
+ S.showBatchTimer = setTimeout(() => {
635
+ S.showBatchTimer = 0;
636
+ drainQueue();
637
+ }, BATCH_FLUSH_MS);
566
638
  }
567
639
 
568
640
  function drainQueue() {
@@ -577,10 +649,12 @@ function drainQueue() {
577
649
  const id = S.pending.shift();
578
650
  S.pendingSet.delete(id);
579
651
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
652
+ if (!phEl(id)?.isConnected) continue;
580
653
  seen.add(id);
581
654
  picked.push(id);
582
655
  }
583
656
  if (picked.length) startShowBatch(picked);
657
+ if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
584
658
  }
585
659
 
586
660
  function startShowBatch(ids) {
@@ -595,7 +669,7 @@ function startShowBatch(ids) {
595
669
  S.inflight = Math.max(0, S.inflight - reserve);
596
670
  drainQueue();
597
671
  };
598
- const timer = setTimeout(release, 7000);
672
+ const timer = setTimeout(release, SHOW_FAILSAFE_MS);
599
673
 
600
674
  requestAnimationFrame(() => {
601
675
  try {
@@ -607,10 +681,8 @@ function startShowBatch(ids) {
607
681
  for (const raw of ids) {
608
682
  const id = parseInt(raw, 10);
609
683
  if (!Number.isFinite(id) || id <= 0) continue;
610
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
611
- if (!ph?.isConnected || isFilled(ph)) continue;
612
- if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) continue;
613
- if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
684
+ const ph = phEl(id);
685
+ if (!canShowPlaceholderId(id, t)) continue;
614
686
 
615
687
  S.lastShow.set(id, t);
616
688
  try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
@@ -625,9 +697,8 @@ function startShowBatch(ids) {
625
697
  try { ez.showAds(...valid); } catch (_) {}
626
698
  for (const id of valid) {
627
699
  S.ezActiveIds.add(id);
628
- scheduleEmptyCheck(id, t);
629
700
  }
630
- setTimeout(() => { clearTimeout(timer); release(); }, 700);
701
+ setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
631
702
  };
632
703
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
633
704
  } catch (_) { clearTimeout(timer); release(); }
@@ -635,18 +706,6 @@ function startShowBatch(ids) {
635
706
  }
636
707
 
637
708
 
638
- function scheduleEmptyCheck(id, showTs) {
639
- setTimeout(() => {
640
- try {
641
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
642
- const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
643
- if (!wrap || !ph?.isConnected) return;
644
- if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
645
- wrap.classList.toggle('is-empty', !isFilled(ph));
646
- } catch (_) {}
647
- }, EMPTY_CHECK_MS);
648
- }
649
-
650
709
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
651
710
  //
652
711
  // Intercepte ez.showAds() pour :
@@ -669,8 +728,7 @@ function startShowBatch(ids) {
669
728
  for (const v of ids) {
670
729
  const id = parseInt(v, 10);
671
730
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
672
- if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
673
- if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
731
+ if (!phEl(id)?.isConnected || !hasSinglePlaceholder(id)) continue;
674
732
  seen.add(id);
675
733
  valid.push(id);
676
734
  }
@@ -695,6 +753,7 @@ function startShowBatch(ids) {
695
753
  async function runCore() {
696
754
  if (isBlocked()) return 0;
697
755
  patchShowAds();
756
+ sweepDeadWraps();
698
757
 
699
758
  const cfg = await fetchConfig();
700
759
  if (!cfg || cfg.excluded) return 0;
@@ -761,7 +820,7 @@ function startShowBatch(ids) {
761
820
  S.burstCount++;
762
821
  scheduleRun(n => {
763
822
  if (!n && !S.pending.length) { S.burstActive = false; return; }
764
- setTimeout(step, n > 0 ? 150 : 300);
823
+ setTimeout(step, n > 0 ? 80 : 180);
765
824
  });
766
825
  };
767
826
  step();
@@ -783,8 +842,10 @@ function startShowBatch(ids) {
783
842
  S.inflight = 0;
784
843
  S.pending = [];
785
844
  S.pendingSet.clear();
845
+ if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
786
846
  S.burstActive = false;
787
847
  S.runQueued = false;
848
+ S.sweepQueued = false;
788
849
  S.scrollSpeed = 0;
789
850
  S.lastScrollY = 0;
790
851
  S.lastScrollTs = 0;
@@ -798,6 +859,14 @@ function startShowBatch(ids) {
798
859
  S.domObs = new MutationObserver(muts => {
799
860
  if (S.mutGuard > 0 || isBlocked()) return;
800
861
  for (const m of muts) {
862
+ let sawWrapRemoval = false;
863
+ for (const n of m.removedNodes) {
864
+ if (n.nodeType !== 1) continue;
865
+ if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || (n.querySelector && n.querySelector(`.${WRAP_CLASS}`))) {
866
+ sawWrapRemoval = true;
867
+ }
868
+ }
869
+ if (sawWrapRemoval) queueSweepDeadWraps();
801
870
  for (const n of m.addedNodes) {
802
871
  if (n.nodeType !== 1) continue;
803
872
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
@@ -885,7 +954,7 @@ function startShowBatch(ids) {
885
954
  S.pageKey = pageKey();
886
955
  blockedUntil = 0;
887
956
  muteConsole(); ensureTcfLocator(); warmNetwork();
888
- patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
957
+ patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
889
958
  });
890
959
 
891
960
  const burstEvts = [
package/public/style.css CHANGED
@@ -56,21 +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
-
74
59
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
75
60
  .ezoic-ad {
76
61
  margin: 0 !important;