nodebb-plugin-ezoic-infinite 1.8.0 → 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.8.0",
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,16 +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
+ // Tunables (stables en prod)
80
81
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
81
- const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
82
- 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)
83
84
  const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
84
- const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
85
- 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
86
90
 
87
91
  // Marges IO larges et fixes — observer créé une seule fois au boot
88
- const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
89
- 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';
90
94
 
91
95
  const SEL = {
92
96
  post: '[component="post"][data-pid]',
@@ -126,6 +130,8 @@
126
130
  inflight: 0, // showAds() en cours
127
131
  pending: [], // ids en attente de slot inflight
128
132
  pendingSet: new Set(),
133
+ showBatchTimer: 0,
134
+ sweepQueued: false,
129
135
  wrapByKey: new Map(), // anchorKey → wrap DOM node
130
136
  ezActiveIds: new Set(), // ids déjà passés à showAds/displayMore
131
137
  scrollDir: 1, // 1=bas, -1=haut
@@ -147,6 +153,33 @@
147
153
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
148
154
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
149
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
+
150
183
  function getDynamicShowBatchMax() {
151
184
  const speed = S.scrollSpeed || 0;
152
185
  const pend = S.pending.length;
@@ -329,6 +362,25 @@ function destroyEzoicId(id) {
329
362
  return null;
330
363
  }
331
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
+
332
384
  /**
333
385
  * Pool épuisé : recycle un wrap loin au-dessus du viewport.
334
386
  * Séquence avec délais (destroyPlaceholders est asynchrone) :
@@ -352,13 +404,14 @@ function recycleAndMove(klass, targetEl, newKey) {
352
404
  let bestAnyEmpty = null, bestAnyMetric = Infinity;
353
405
  let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
354
406
 
355
- 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;
356
409
  try {
357
410
  const rect = wrap.getBoundingClientRect();
358
411
  const isAbove = rect.bottom <= farAbove;
359
412
  const isBelow = rect.top >= farBelow;
360
413
  const anyFar = isAbove || isBelow;
361
- if (!anyFar) return;
414
+ if (!anyFar) continue;
362
415
 
363
416
  const qualifies = preferAbove ? isAbove : isBelow;
364
417
  const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
@@ -377,7 +430,7 @@ function recycleAndMove(klass, targetEl, newKey) {
377
430
  if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
378
431
  }
379
432
  } catch (_) {}
380
- });
433
+ }
381
434
 
382
435
  const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
383
436
  if (!best) return null;
@@ -522,7 +575,8 @@ function recycleAndMove(klass, targetEl, newKey) {
522
575
  const key = anchorKey(klass, el);
523
576
  if (findWrap(key)) continue;
524
577
 
525
- const id = pickId(poolKey);
578
+ let id = pickId(poolKey);
579
+ if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
526
580
  if (id) {
527
581
  const w = insertAfter(el, id, klass, key);
528
582
  if (w) { observePh(id); inserted++; }
@@ -553,15 +607,34 @@ function recycleAndMove(klass, targetEl, newKey) {
553
607
  }
554
608
 
555
609
  function observePh(id) {
556
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
610
+ const ph = phEl(id);
557
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 (_) {}
558
620
  }
559
621
 
560
622
  function enqueueShow(id) {
561
623
  if (!id || isBlocked()) return;
562
- if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
563
- if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
564
- 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);
565
638
  }
566
639
 
567
640
  function drainQueue() {
@@ -576,10 +649,12 @@ function drainQueue() {
576
649
  const id = S.pending.shift();
577
650
  S.pendingSet.delete(id);
578
651
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
652
+ if (!phEl(id)?.isConnected) continue;
579
653
  seen.add(id);
580
654
  picked.push(id);
581
655
  }
582
656
  if (picked.length) startShowBatch(picked);
657
+ if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
583
658
  }
584
659
 
585
660
  function startShowBatch(ids) {
@@ -594,7 +669,7 @@ function startShowBatch(ids) {
594
669
  S.inflight = Math.max(0, S.inflight - reserve);
595
670
  drainQueue();
596
671
  };
597
- const timer = setTimeout(release, 7000);
672
+ const timer = setTimeout(release, SHOW_FAILSAFE_MS);
598
673
 
599
674
  requestAnimationFrame(() => {
600
675
  try {
@@ -606,10 +681,8 @@ function startShowBatch(ids) {
606
681
  for (const raw of ids) {
607
682
  const id = parseInt(raw, 10);
608
683
  if (!Number.isFinite(id) || id <= 0) continue;
609
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
610
- if (!ph?.isConnected || isFilled(ph)) continue;
611
- if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) continue;
612
- if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
684
+ const ph = phEl(id);
685
+ if (!canShowPlaceholderId(id, t)) continue;
613
686
 
614
687
  S.lastShow.set(id, t);
615
688
  try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
@@ -625,7 +698,7 @@ function startShowBatch(ids) {
625
698
  for (const id of valid) {
626
699
  S.ezActiveIds.add(id);
627
700
  }
628
- setTimeout(() => { clearTimeout(timer); release(); }, 700);
701
+ setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
629
702
  };
630
703
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
631
704
  } catch (_) { clearTimeout(timer); release(); }
@@ -655,8 +728,7 @@ function startShowBatch(ids) {
655
728
  for (const v of ids) {
656
729
  const id = parseInt(v, 10);
657
730
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
658
- if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
659
- if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
731
+ if (!phEl(id)?.isConnected || !hasSinglePlaceholder(id)) continue;
660
732
  seen.add(id);
661
733
  valid.push(id);
662
734
  }
@@ -681,6 +753,7 @@ function startShowBatch(ids) {
681
753
  async function runCore() {
682
754
  if (isBlocked()) return 0;
683
755
  patchShowAds();
756
+ sweepDeadWraps();
684
757
 
685
758
  const cfg = await fetchConfig();
686
759
  if (!cfg || cfg.excluded) return 0;
@@ -747,7 +820,7 @@ function startShowBatch(ids) {
747
820
  S.burstCount++;
748
821
  scheduleRun(n => {
749
822
  if (!n && !S.pending.length) { S.burstActive = false; return; }
750
- setTimeout(step, n > 0 ? 150 : 300);
823
+ setTimeout(step, n > 0 ? 80 : 180);
751
824
  });
752
825
  };
753
826
  step();
@@ -769,8 +842,10 @@ function startShowBatch(ids) {
769
842
  S.inflight = 0;
770
843
  S.pending = [];
771
844
  S.pendingSet.clear();
845
+ if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
772
846
  S.burstActive = false;
773
847
  S.runQueued = false;
848
+ S.sweepQueued = false;
774
849
  S.scrollSpeed = 0;
775
850
  S.lastScrollY = 0;
776
851
  S.lastScrollTs = 0;
@@ -784,6 +859,14 @@ function startShowBatch(ids) {
784
859
  S.domObs = new MutationObserver(muts => {
785
860
  if (S.mutGuard > 0 || isBlocked()) return;
786
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();
787
870
  for (const n of m.addedNodes) {
788
871
  if (n.nodeType !== 1) continue;
789
872
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
@@ -871,7 +954,7 @@ function startShowBatch(ids) {
871
954
  S.pageKey = pageKey();
872
955
  blockedUntil = 0;
873
956
  muteConsole(); ensureTcfLocator(); warmNetwork();
874
- patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
957
+ patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
875
958
  });
876
959
 
877
960
  const burstEvts = [
package/public/style.css CHANGED
@@ -56,12 +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
-
65
59
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
66
60
  .ezoic-ad {
67
61
  margin: 0 !important;