nodebb-plugin-ezoic-infinite 1.5.30 → 1.5.31

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 +95 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.30",
3
+ "version": "1.5.31",
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
@@ -10,8 +10,24 @@
10
10
  // Insert at most N ads per run to keep the UI smooth on infinite scroll
11
11
  const MAX_INSERTS_PER_RUN = 3;
12
12
 
13
- // Preload before viewport (tune if you want even earlier)
14
- const PRELOAD_ROOT_MARGIN = '1200px 0px';
13
+ // Preload before viewport (earlier load for smoother scroll)
14
+ const PRELOAD_MARGIN_DESKTOP = '1600px 0px 1600px 0px';
15
+ const PRELOAD_MARGIN_MOBILE = '900px 0px 900px 0px';
16
+
17
+ const MAX_INFLIGHT_DESKTOP = 3;
18
+ const MAX_INFLIGHT_MOBILE = 2;
19
+
20
+ function isMobile() {
21
+ try { return window && window.innerWidth && window.innerWidth < 768; } catch (e) { return false; }
22
+ }
23
+
24
+ function getPreloadRootMargin() {
25
+ return isMobile() ? PRELOAD_MARGIN_MOBILE : PRELOAD_MARGIN_DESKTOP;
26
+ }
27
+
28
+ function getMaxInflight() {
29
+ return isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
30
+ }
15
31
 
16
32
  const SELECTORS = {
17
33
  topicItem: 'li[component="category/topic"]',
@@ -50,22 +66,64 @@
50
66
  io: null,
51
67
  runQueued: false,
52
68
 
53
- // hero
69
+ // preloading budget
70
+ inflight: 0,
71
+ pending: [],
72
+ pendingSet: new Set(),
73
+
74
+ // hero)
54
75
  heroDoneForPage: false,
55
76
  };
56
77
 
57
78
  const insertingIds = new Set();
58
79
 
59
- // Debug logs (enable with localStorage.ezoicInfiniteDebug = "1")
60
- function dbg(...args) {
61
- try {
62
- if (window && window.localStorage && window.localStorage.getItem('ezoicInfiniteDebug') === '1') {
63
- // eslint-disable-next-line no-console
64
- console.log('[ezoicInfinite]', ...args);
80
+ function enqueueShow(id) {
81
+ if (!id || isBlocked()) return;
82
+ const max = getMaxInflight();
83
+ if (state.inflight >= max) {
84
+ if (!state.pendingSet.has(id)) {
85
+ state.pending.push(id);
86
+ state.pendingSet.add(id);
65
87
  }
88
+ return;
89
+ }
90
+ startShow(id);
91
+ }
92
+
93
+ function drainQueue() {
94
+ if (isBlocked()) return;
95
+ const max = getMaxInflight();
96
+ while (state.inflight < max && state.pending.length) {
97
+ const id = state.pending.shift();
98
+ state.pendingSet.delete(id);
99
+ startShow(id);
100
+ }
101
+ }
102
+
103
+ function markEmptyWrapper(id) {
104
+ try {
105
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
106
+ if (!ph || !ph.isConnected) return;
107
+ const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
108
+ if (!wrap) return;
109
+ // If still empty after a delay, collapse it.
110
+ setTimeout(() => {
111
+ try {
112
+ const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
113
+ if (!ph2 || !ph2.isConnected) return;
114
+ const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
115
+ if (!w2) return;
116
+ // consider empty if only whitespace and no iframes/ins/img
117
+ const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
118
+ if (!hasAd) w2.classList.add('is-empty');
119
+ } catch (e) {}
120
+ }, 3500);
66
121
  } catch (e) {}
67
122
  }
68
123
 
124
+ // Production build: debug disabled
125
+ function dbg() {}
126
+
69
127
  // ---------- small utils ----------
70
128
 
71
129
  function normalizeBool(v) {
@@ -408,13 +466,28 @@ function buildWrap(id, kindClass, afterPos) {
408
466
  }
409
467
  }
410
468
 
411
- function showAd(id) {
469
+ function enqueueShow(id) {
412
470
  if (!id || isBlocked()) return;
413
471
 
414
472
  const now = Date.now();
415
473
  const last = state.lastShowById.get(id) || 0;
416
474
  if (now - last < 1500) return; // basic throttle
417
475
 
476
+ // Defer one frame
477
+
478
+ // Budget concurrent loads
479
+ state.inflight++;
480
+ let released = false;
481
+ const release = () => {
482
+ if (released) return;
483
+ released = true;
484
+ state.inflight = Math.max(0, state.inflight - 1);
485
+ drainQueue();
486
+ };
487
+
488
+ // Safety release in case Ezoic never fills
489
+ const hardTimer = setTimeout(release, 6500);
490
+
418
491
  // Defer one frame so the placeholder is definitely in DOM after insertion/recycle
419
492
  requestAnimationFrame(() => {
420
493
  if (isBlocked()) return;
@@ -440,6 +513,9 @@ function buildWrap(id, kindClass, afterPos) {
440
513
  } catch (e) {}
441
514
  try { ez.showAds(id); } catch (e) {}
442
515
  try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
516
+ try { markEmptyWrapper(id); } catch (e) {}
517
+ // allow a short time for DOM fill; then release budget
518
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 700);
443
519
  };
444
520
 
445
521
  // Fast path
@@ -466,10 +542,12 @@ function buildWrap(id, kindClass, afterPos) {
466
542
  } catch (e) {}
467
543
  try { ez2.showAds(id); } catch (e) {}
468
544
  try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
545
+ try { markEmptyWrapper(id); } catch (e) {}
546
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 700);
469
547
  } catch (e) {}
470
548
  });
471
549
  }
472
- } catch (e) {}
550
+ } catch (e) { try { clearTimeout(hardTimer); release(); } catch (e2) {} }
473
551
  });
474
552
  }
475
553
 
@@ -487,9 +565,9 @@ function buildWrap(id, kindClass, afterPos) {
487
565
 
488
566
  const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
489
567
  const id = parseInt(idAttr, 10);
490
- if (Number.isFinite(id) && id > 0) showAd(id);
568
+ if (Number.isFinite(id) && id > 0) enqueueShow(id);
491
569
  }
492
- }, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
570
+ }, { root: null, rootMargin: getPreloadRootMargin(), threshold: 0 });
493
571
  } catch (e) {
494
572
  state.io = null;
495
573
  }
@@ -505,7 +583,7 @@ function buildWrap(id, kindClass, afterPos) {
505
583
  // If already above fold, fire immediately
506
584
  try {
507
585
  const r = ph.getBoundingClientRect();
508
- if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
586
+ if (r.top < window.innerHeight * 1.5 && r.bottom > -200) enqueueShow(id);
509
587
  } catch (e) {}
510
588
  }
511
589
 
@@ -708,6 +786,9 @@ function buildWrap(id, kindClass, afterPos) {
708
786
  state.curPosts = 0;
709
787
  state.curCategories = 0;
710
788
  state.lastShowById.clear();
789
+ state.inflight = 0;
790
+ state.pending = [];
791
+ try { state.pendingSet && state.pendingSet.clear(); } catch (e) {}
711
792
  try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
712
793
  state.heroDoneForPage = false;
713
794