nodebb-plugin-ezoic-infinite 1.5.30 → 1.5.32

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 +120 -62
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.32",
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 = '2600px 0px 2600px 0px';
15
+ const PRELOAD_MARGIN_MOBILE = '1500px 0px 1500px 0px';
16
+
17
+ const MAX_INFLIGHT_DESKTOP = 4;
18
+ const MAX_INFLIGHT_MOBILE = 3;
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,43 @@
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) {
80
+ }
81
+
82
+ function markEmptyWrapper(id) {
61
83
  try {
62
- if (window && window.localStorage && window.localStorage.getItem('ezoicInfiniteDebug') === '1') {
63
- // eslint-disable-next-line no-console
64
- console.log('[ezoicInfinite]', ...args);
65
- }
84
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
85
+ if (!ph || !ph.isConnected) return;
86
+ const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
87
+ if (!wrap) return;
88
+ // If still empty after a delay, collapse it.
89
+ setTimeout(() => {
90
+ try {
91
+ const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
92
+ if (!ph2 || !ph2.isConnected) return;
93
+ const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
94
+ if (!w2) return;
95
+ // consider empty if only whitespace and no iframes/ins/img
96
+ const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
97
+ if (!hasAd) w2.classList.add('is-empty');
98
+ } catch (e) {}
99
+ }, 3500);
66
100
  } catch (e) {}
67
101
  }
68
102
 
103
+ // Production build: debug disabled
104
+ function dbg() {}
105
+
69
106
  // ---------- small utils ----------
70
107
 
71
108
  function normalizeBool(v) {
@@ -408,15 +445,51 @@ function buildWrap(id, kindClass, afterPos) {
408
445
  }
409
446
  }
410
447
 
411
- function showAd(id) {
412
- if (!id || isBlocked()) return;
448
+ function enqueueShow(id) {
449
+ if (!id || isBlocked()) return;
413
450
 
414
- const now = Date.now();
415
- const last = state.lastShowById.get(id) || 0;
416
- if (now - last < 1500) return; // basic throttle
451
+ // Basic per-id throttle (prevents rapid re-requests when DOM churns)
452
+ const now = Date.now();
453
+ const last = state.lastShowById.get(id) || 0;
454
+ if (now - last < 900) return;
455
+
456
+ const max = getMaxInflight();
457
+ if (state.inflight >= max) {
458
+ if (!state.pendingSet.has(id)) {
459
+ state.pending.push(id);
460
+ state.pendingSet.add(id);
461
+ }
462
+ return;
463
+ }
464
+ startShow(id);
465
+ }
466
+
467
+ function drainQueue() {
468
+ if (isBlocked()) return;
469
+ const max = getMaxInflight();
470
+ while (state.inflight < max && state.pending.length) {
471
+ const id = state.pending.shift();
472
+ state.pendingSet.delete(id);
473
+ startShow(id);
474
+ }
475
+ }
476
+
477
+ function startShow(id) {
478
+ if (!id || isBlocked()) return;
479
+
480
+ state.inflight++;
481
+ let released = false;
482
+ const release = () => {
483
+ if (released) return;
484
+ released = true;
485
+ state.inflight = Math.max(0, state.inflight - 1);
486
+ drainQueue();
487
+ };
488
+
489
+ const hardTimer = setTimeout(release, 6500);
417
490
 
418
- // Defer one frame so the placeholder is definitely in DOM after insertion/recycle
419
- requestAnimationFrame(() => {
491
+ requestAnimationFrame(() => {
492
+ try {
420
493
  if (isBlocked()) return;
421
494
 
422
495
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
@@ -424,54 +497,36 @@ function buildWrap(id, kindClass, afterPos) {
424
497
 
425
498
  const now2 = Date.now();
426
499
  const last2 = state.lastShowById.get(id) || 0;
427
- if (now2 - last2 < 1200) return;
500
+ if (now2 - last2 < 900) return;
428
501
  state.lastShowById.set(id, now2);
429
502
 
430
- try {
431
- window.ezstandalone = window.ezstandalone || {};
432
- const ez = window.ezstandalone;
503
+ window.ezstandalone = window.ezstandalone || {};
504
+ const ez = window.ezstandalone;
433
505
 
434
- const doShow = () => {
435
- try {
436
- if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
437
- // Avoid Ezoic caching state for reused placeholders
438
- ez.destroyPlaceholders(id);
439
- }
440
- } catch (e) {}
441
- try { ez.showAds(id); } catch (e) {}
442
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
443
- };
506
+ const doShow = () => {
507
+ try {
508
+ if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
509
+ try { ez.destroyPlaceholders(id); } catch (e) {}
510
+ }
511
+ } catch (e) {}
444
512
 
445
- // Fast path
446
- if (typeof ez.showAds === 'function') {
447
- doShow();
448
- return;
449
- }
513
+ try { ez.showAds(id); } catch (e) {}
514
+ try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
515
+ try { markEmptyWrapper(id); } catch (e) {}
450
516
 
451
- // Queue once for when Ezoic is ready
452
- ez.cmd = ez.cmd || [];
453
- if (!ph.__ezoicQueued) {
454
- ph.__ezoicQueued = true;
455
- ez.cmd.push(() => {
456
- try {
457
- if (isBlocked()) return;
458
- const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
459
- if (!el || !el.isConnected) return;
460
- const ez2 = window.ezstandalone;
461
- if (!ez2 || typeof ez2.showAds !== 'function') return;
462
- try {
463
- if (state.usedOnce && state.usedOnce.has(id) && typeof ez2.destroyPlaceholders === 'function') {
464
- ez2.destroyPlaceholders(id);
465
- }
466
- } catch (e) {}
467
- try { ez2.showAds(id); } catch (e) {}
468
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
469
- } catch (e) {}
470
- });
471
- }
472
- } catch (e) {}
473
- });
474
- }
517
+ setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
518
+ };
519
+
520
+ if (Array.isArray(ez.cmd)) {
521
+ try { ez.cmd.push(doShow); } catch (e) { doShow(); }
522
+ } else {
523
+ doShow();
524
+ }
525
+ } finally {
526
+ // If we returned early, hardTimer will release.
527
+ }
528
+ });
529
+ }
475
530
 
476
531
 
477
532
  // ---------- preload / above-the-fold ----------
@@ -487,9 +542,9 @@ function buildWrap(id, kindClass, afterPos) {
487
542
 
488
543
  const idAttr = el && el.getAttribute && el.getAttribute('data-ezoic-id');
489
544
  const id = parseInt(idAttr, 10);
490
- if (Number.isFinite(id) && id > 0) showAd(id);
545
+ if (Number.isFinite(id) && id > 0) enqueueShow(id);
491
546
  }
492
- }, { root: null, rootMargin: PRELOAD_ROOT_MARGIN, threshold: 0.01 });
547
+ }, { root: null, rootMargin: getPreloadRootMargin(), threshold: 0 });
493
548
  } catch (e) {
494
549
  state.io = null;
495
550
  }
@@ -505,7 +560,7 @@ function buildWrap(id, kindClass, afterPos) {
505
560
  // If already above fold, fire immediately
506
561
  try {
507
562
  const r = ph.getBoundingClientRect();
508
- if (r.top < window.innerHeight * 1.5 && r.bottom > -200) showAd(id);
563
+ if (r.top < window.innerHeight * 3.0 && r.bottom > -800) enqueueShow(id);
509
564
  } catch (e) {}
510
565
  }
511
566
 
@@ -708,6 +763,9 @@ function buildWrap(id, kindClass, afterPos) {
708
763
  state.curPosts = 0;
709
764
  state.curCategories = 0;
710
765
  state.lastShowById.clear();
766
+ state.inflight = 0;
767
+ state.pending = [];
768
+ try { state.pendingSet && state.pendingSet.clear(); } catch (e) {}
711
769
  try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
712
770
  state.heroDoneForPage = false;
713
771