nodebb-plugin-ezoic-infinite 1.5.29 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.29",
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) {
@@ -286,8 +344,15 @@ function withInternalDomChange(fn) {
286
344
  let removed = 0;
287
345
 
288
346
  wraps.forEach((wrap) => {
289
- const prev = wrap.previousElementSibling;
290
- if (!prev || !itemSet.has(prev)) {
347
+ // NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
348
+ let ok = false;
349
+ let prev = wrap.previousElementSibling;
350
+ for (let i = 0; i < 3 && prev; i++) {
351
+ if (itemSet.has(prev)) { ok = true; break; }
352
+ prev = prev.previousElementSibling;
353
+ }
354
+
355
+ if (!ok) {
291
356
  const id = getWrapIdFromWrap(wrap);
292
357
  withInternalDomChange(() => {
293
358
  try {
@@ -315,12 +380,12 @@ function withInternalDomChange(fn) {
315
380
  if (hasContent) wrap.classList.remove('is-empty');
316
381
  else wrap.classList.add('is-empty');
317
382
  } catch (e) {}
318
- }, 1800);
383
+ }, 3500);
319
384
  }
320
385
 
321
386
  function buildWrap(id, kindClass, afterPos) {
322
387
  const wrap = document.createElement('div');
323
- wrap.className = `${WRAP_CLASS} ${kindClass} is-empty`;
388
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
324
389
  wrap.setAttribute('data-ezoic-after', String(afterPos));
325
390
  wrap.setAttribute('data-ezoic-wrapid', String(id));
326
391
  wrap.style.width = '100%';
@@ -401,13 +466,28 @@ function buildWrap(id, kindClass, afterPos) {
401
466
  }
402
467
  }
403
468
 
404
- function showAd(id) {
469
+ function enqueueShow(id) {
405
470
  if (!id || isBlocked()) return;
406
471
 
407
472
  const now = Date.now();
408
473
  const last = state.lastShowById.get(id) || 0;
409
474
  if (now - last < 1500) return; // basic throttle
410
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
+
411
491
  // Defer one frame so the placeholder is definitely in DOM after insertion/recycle
412
492
  requestAnimationFrame(() => {
413
493
  if (isBlocked()) return;
@@ -433,6 +513,9 @@ function buildWrap(id, kindClass, afterPos) {
433
513
  } catch (e) {}
434
514
  try { ez.showAds(id); } catch (e) {}
435
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);
436
519
  };
437
520
 
438
521
  // Fast path
@@ -459,10 +542,12 @@ function buildWrap(id, kindClass, afterPos) {
459
542
  } catch (e) {}
460
543
  try { ez2.showAds(id); } catch (e) {}
461
544
  try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
545
+ try { markEmptyWrapper(id); } catch (e) {}
546
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 700);
462
547
  } catch (e) {}
463
548
  });
464
549
  }
465
- } catch (e) {}
550
+ } catch (e) { try { clearTimeout(hardTimer); release(); } catch (e2) {} }
466
551
  });
467
552
  }
468
553
 
@@ -480,9 +565,9 @@ function buildWrap(id, kindClass, afterPos) {
480
565
 
481
566
  const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
482
567
  const id = parseInt(idAttr, 10);
483
- if (Number.isFinite(id) && id > 0) showAd(id);
568
+ if (Number.isFinite(id) && id > 0) enqueueShow(id);
484
569
  }
485
- }, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
570
+ }, { root: null, rootMargin: getPreloadRootMargin(), threshold: 0 });
486
571
  } catch (e) {
487
572
  state.io = null;
488
573
  }
@@ -498,7 +583,7 @@ function buildWrap(id, kindClass, afterPos) {
498
583
  // If already above fold, fire immediately
499
584
  try {
500
585
  const r = ph.getBoundingClientRect();
501
- 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);
502
587
  } catch (e) {}
503
588
  }
504
589
 
@@ -701,6 +786,9 @@ function buildWrap(id, kindClass, afterPos) {
701
786
  state.curPosts = 0;
702
787
  state.curCategories = 0;
703
788
  state.lastShowById.clear();
789
+ state.inflight = 0;
790
+ state.pending = [];
791
+ try { state.pendingSet && state.pendingSet.clear(); } catch (e) {}
704
792
  try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
705
793
  state.heroDoneForPage = false;
706
794
 
package/public/style.css CHANGED
@@ -23,11 +23,12 @@
23
23
 
24
24
  /* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
25
25
  .ezoic-ad.is-empty {
26
- display: none !important;
26
+ display: block !important;
27
27
  margin: 0 !important;
28
28
  padding: 0 !important;
29
29
  height: 0 !important;
30
30
  min-height: 0 !important;
31
+ overflow: hidden !important;
31
32
  }
32
33
 
33
34
  .ezoic-ad {