nodebb-plugin-ezoic-infinite 1.5.36 → 1.5.38

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 +157 -36
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.36",
3
+ "version": "1.5.38",
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
@@ -8,23 +8,24 @@
8
8
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
9
9
 
10
10
  // Insert at most N ads per run to keep the UI smooth on infinite scroll
11
- const MAX_INSERTS_PER_RUN = 3;
11
+ const MAX_INSERTS_PER_RUN = 6;
12
12
 
13
13
  // Preload before viewport (earlier load for smoother scroll)
14
14
  // Preload far enough ahead that fast scroll doesn't outrun ad loading.
15
- const PRELOAD_MARGIN_DESKTOP = '3200px 0px 3200px 0px';
16
- const PRELOAD_MARGIN_MOBILE = '1800px 0px 1800px 0px';
15
+ // Slightly more aggressive margins to reduce “I scrolled past it before it loaded”.
16
+ const PRELOAD_MARGIN_DESKTOP = '6000px 0px 6000px 0px';
17
+ const PRELOAD_MARGIN_MOBILE = '3500px 0px 3500px 0px';
17
18
 
18
19
  // When the user scrolls very fast, temporarily preload more aggressively.
19
20
  // This helps ensure ads are already in-flight before the user reaches them.
20
- const PRELOAD_MARGIN_DESKTOP_BOOST = '5200px 0px 5200px 0px';
21
- const PRELOAD_MARGIN_MOBILE_BOOST = '3200px 0px 3200px 0px';
21
+ const PRELOAD_MARGIN_DESKTOP_BOOST = '8500px 0px 8500px 0px';
22
+ const PRELOAD_MARGIN_MOBILE_BOOST = '5500px 0px 5500px 0px';
22
23
  const BOOST_DURATION_MS = 2500;
23
24
  const BOOST_SPEED_PX_PER_MS = 2.2; // ~2200px/s
24
25
 
25
26
  // Allow a bit more parallelism; the perf profile can still dial this down on low-end devices.
26
- const MAX_INFLIGHT_DESKTOP = 5;
27
- const MAX_INFLIGHT_MOBILE = 4;
27
+ const MAX_INFLIGHT_DESKTOP = 8;
28
+ const MAX_INFLIGHT_MOBILE = 6;
28
29
 
29
30
 
30
31
  // Adaptive performance profile (device/network aware)
@@ -574,45 +575,155 @@ function startShow(id) {
574
575
  drainQueue();
575
576
  };
576
577
 
578
+ // Safety release even if the ad pipeline stalls
577
579
  const hardTimer = setTimeout(release, 6500);
578
580
 
579
- requestAnimationFrame(() => {
580
- try {
581
- if (isBlocked()) return;
581
+ try {
582
+ if (isBlocked()) return;
582
583
 
583
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
584
- if (!ph || !ph.isConnected) return;
584
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
585
+ if (!ph || !ph.isConnected) return;
585
586
 
586
- const now2 = Date.now();
587
- const last2 = state.lastShowById.get(id) || 0;
588
- if (now2 - last2 < 900) return;
589
- state.lastShowById.set(id, now2);
587
+ // Guard against rapid duplicate calls for the same id
588
+ const now2 = Date.now();
589
+ const last2 = state.lastShowById.get(id) || 0;
590
+ if (now2 - last2 < 250) return;
591
+ state.lastShowById.set(id, now2);
590
592
 
591
- window.ezstandalone = window.ezstandalone || {};
592
- const ez = window.ezstandalone;
593
+ window.ezstandalone = window.ezstandalone || {};
594
+ const ez = window.ezstandalone;
593
595
 
594
- const doShow = () => {
595
- try {
596
- if (state.usedOnce && state.usedOnce.has(id)) {
597
- safeDestroyById(id);
598
- }
599
- } catch (e) {}
596
+ const doShow = () => {
597
+ try {
598
+ if (state.usedOnce && state.usedOnce.has(id)) {
599
+ safeDestroyById(id);
600
+ }
601
+ } catch (e) {}
602
+
603
+ try { ez.showAds(id); } catch (e) {}
604
+ try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
605
+
606
+ // Tighten any oversized reserved height once the creative is in the DOM.
607
+ try { scheduleTighten(id); } catch (e) {}
600
608
 
601
- try { ez.showAds(id); } catch (e) {}
602
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
609
+ // Release budget quickly; the creative can keep loading independently.
610
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 300);
611
+ };
612
+
613
+ // Use cmd queue if present, but don't delay to next frame.
614
+ if (Array.isArray(ez.cmd)) {
615
+ try { ez.cmd.push(doShow); } catch (e) { doShow(); }
616
+ } else {
617
+ doShow();
618
+ }
619
+ } finally {
620
+ // If we returned early, hardTimer will release.
621
+ }
622
+ }
603
623
 
604
- setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
605
- };
606
624
 
607
- if (Array.isArray(ez.cmd)) {
608
- try { ez.cmd.push(doShow); } catch (e) { doShow(); }
609
- } else {
610
- doShow();
625
+ // ---------- height normalization (reduce empty space) ----------
626
+ // Some Ezoic / GPT wrappers reserve a larger min-height (e.g., 400px) than the rendered creative (e.g., 250px),
627
+ // leaving visible empty space. We “tighten” the reserved min-height to the actual iframe/container height after load.
628
+ const _heightObsByPlaceholder = new Map();
629
+
630
+ function tightenEzoicHeightFor(id) {
631
+ try {
632
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
633
+ if (!ph || !ph.isConnected) return;
634
+
635
+ const ad = ph.querySelector('span.ezoic-ad');
636
+ if (!ad) return;
637
+
638
+ // Prefer the safeframe/container size if present.
639
+ let h = 0;
640
+ const container = ad.querySelector('div[id$="__container__"]');
641
+ if (container) {
642
+ const r = container.getBoundingClientRect();
643
+ if (r && r.height) h = Math.max(h, r.height);
644
+ }
645
+ const iframe = ad.querySelector('iframe');
646
+ if (iframe) {
647
+ const r2 = iframe.getBoundingClientRect();
648
+ if (r2 && r2.height) h = Math.max(h, r2.height);
649
+ const attrH = parseInt(iframe.getAttribute('height') || '', 10);
650
+ if (Number.isFinite(attrH) && attrH > 0) h = Math.max(h, attrH);
651
+ }
652
+
653
+ // Fall back to inner wrapper height.
654
+ if (!h) {
655
+ const inner = ad.firstElementChild;
656
+ if (inner) {
657
+ const r3 = inner.getBoundingClientRect();
658
+ if (r3 && r3.height) h = Math.max(h, r3.height);
611
659
  }
612
- } finally {
613
- // If we returned early, hardTimer will release.
614
660
  }
615
- });
661
+
662
+ if (!h || h < 10) return;
663
+ const px = `${Math.ceil(h)}px`;
664
+
665
+ // Override inline min-height with an inline !important value.
666
+ try { ad.style.setProperty('min-height', px, 'important'); } catch (e) {}
667
+ // Keep height auto so responsive creatives can expand if needed.
668
+ try { ad.style.setProperty('height', 'auto', 'important'); } catch (e) {}
669
+ } catch (e) {}
670
+ }
671
+
672
+ function ensureHeightObserver(id) {
673
+ try {
674
+ if (_heightObsByPlaceholder.has(id)) return;
675
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
676
+ if (!ph || !ph.isConnected) return;
677
+
678
+ const ro = (typeof ResizeObserver === 'function') ? new ResizeObserver(() => {
679
+ tightenEzoicHeightFor(id);
680
+ }) : null;
681
+
682
+ // Also watch for Ezoic re-applying an oversized inline min-height (style attribute changes).
683
+ const mo = (typeof MutationObserver === 'function') ? new MutationObserver(() => {
684
+ tightenEzoicHeightFor(id);
685
+ }) : null;
686
+
687
+ const attach = () => {
688
+ const ad = ph.querySelector('span.ezoic-ad[data-ez-name], span.ezoic-ad');
689
+ if (!ad) return false;
690
+
691
+ if (ro) {
692
+ const container = ad.querySelector && ad.querySelector('div[id$="__container__"]');
693
+ const iframe = ad.querySelector && ad.querySelector('iframe');
694
+ try { if (container) ro.observe(container); } catch (e) {}
695
+ try { if (iframe) ro.observe(iframe); } catch (e) {}
696
+ try { ro.observe(ad); } catch (e) {}
697
+ }
698
+ if (mo) {
699
+ try { mo.observe(ad, { attributes: true, attributeFilter: ['style', 'class'] }); } catch (e) {}
700
+ }
701
+ return true;
702
+ };
703
+
704
+ // If the ad wrapper is injected slightly later, observe the placeholder briefly for children.
705
+ if (!attach() && typeof MutationObserver === 'function') {
706
+ try {
707
+ const tmp = new MutationObserver(() => {
708
+ if (attach()) {
709
+ try { tmp.disconnect(); } catch (e) {}
710
+ tightenEzoicHeightFor(id);
711
+ }
712
+ });
713
+ tmp.observe(ph, { childList: true, subtree: true });
714
+ setTimeout(() => { try { tmp.disconnect(); } catch (e) {} }, 3000);
715
+ } catch (e) {}
716
+ }
717
+
718
+ _heightObsByPlaceholder.set(id, { ro: ro || null, mo: mo || null });
719
+ } catch (e) {}
720
+ }
721
+
722
+ function scheduleTighten(id) {
723
+ // Run a couple of times (creatives may resize after initial load)
724
+ try { setTimeout(() => tightenEzoicHeightFor(id), 250); } catch (e) {}
725
+ try { setTimeout(() => tightenEzoicHeightFor(id), 1100); } catch (e) {}
726
+ try { ensureHeightObserver(id); } catch (e) {}
616
727
  }
617
728
 
618
729
 
@@ -696,7 +807,7 @@ function startShow(id) {
696
807
  const targets = computeTargets(items.length, interval, showFirst);
697
808
  let inserted = 0;
698
809
  const perf = getPerfProfile();
699
- const maxInserts = perf.maxInsertsPerRun + (isBoosted() ? 1 : 0);
810
+ const maxInserts = perf.maxInsertsPerRun + (isBoosted() ? 2 : 0);
700
811
 
701
812
  for (const afterPos of targets) {
702
813
  if (inserted >= maxInserts) break;
@@ -875,6 +986,16 @@ function startShow(id) {
875
986
  // reset perf profile cache
876
987
  state.perfProfile = null;
877
988
 
989
+ // disconnect any ResizeObservers used for height tightening
990
+ try {
991
+ for (const v of _heightObsByPlaceholder.values()) {
992
+ try { if (v && v.ro && v.ro.disconnect) v.ro.disconnect(); } catch (e) {}
993
+ try { if (v && v.mo && v.mo.disconnect) v.mo.disconnect(); } catch (e) {}
994
+ try { if (v && v.disconnect) v.disconnect(); } catch (e) {} // backward compat
995
+ }
996
+ _heightObsByPlaceholder.clear();
997
+ } catch (e) {}
998
+
878
999
  // reset state
879
1000
  state.cfg = null;
880
1001
  state.allTopics = [];