nodebb-plugin-ezoic-infinite 1.5.33 → 1.5.34

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.33",
3
+ "version": "1.5.34",
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
@@ -24,7 +24,53 @@
24
24
  const MAX_INFLIGHT_DESKTOP = 4;
25
25
  const MAX_INFLIGHT_MOBILE = 3;
26
26
 
27
- function isBoosted() {
27
+
28
+ // Adaptive performance profile (device/network aware)
29
+ const PERF_DEFAULTS = Object.freeze({
30
+ maxInflightDesktop: MAX_INFLIGHT_DESKTOP,
31
+ maxInflightMobile: MAX_INFLIGHT_MOBILE,
32
+ maxInsertsPerRun: MAX_INSERTS_PER_RUN,
33
+ });
34
+
35
+ function getPerfProfile() {
36
+ // Cache result for this pageview; recomputed on navigation via cleanup()
37
+ if (state.perfProfile) return state.perfProfile;
38
+ const p = {
39
+ maxInflightDesktop: PERF_DEFAULTS.maxInflightDesktop,
40
+ maxInflightMobile: PERF_DEFAULTS.maxInflightMobile,
41
+ maxInsertsPerRun: PERF_DEFAULTS.maxInsertsPerRun,
42
+ };
43
+
44
+ try {
45
+ const mem = typeof navigator !== 'undefined' ? navigator.deviceMemory : undefined; // GB
46
+ const cores = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined;
47
+
48
+ // Conservative tuning for low-end devices
49
+ if (typeof mem === 'number' && mem > 0 && mem <= 2) {
50
+ p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
51
+ p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
52
+ p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
53
+ }
54
+ if (typeof cores === 'number' && cores > 0 && cores <= 4) {
55
+ p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
56
+ p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
57
+ p.maxInsertsPerRun = Math.max(1, p.maxInsertsPerRun - 1);
58
+ }
59
+
60
+ const conn = typeof navigator !== 'undefined' ? navigator.connection : undefined;
61
+ const eff = conn && conn.effectiveType ? String(conn.effectiveType).toLowerCase() : '';
62
+ // Slow connections: don't ramp concurrency too high (keeps page responsive).
63
+ if (eff.includes('2g')) {
64
+ p.maxInflightDesktop = Math.max(2, p.maxInflightDesktop - 1);
65
+ p.maxInflightMobile = Math.max(1, p.maxInflightMobile - 1);
66
+ }
67
+ } catch (e) {}
68
+
69
+ state.perfProfile = p;
70
+ return p;
71
+ }
72
+
73
+ function isBoosted() {
28
74
  try { return Date.now() < (state.scrollBoostUntil || 0); } catch (e) { return false; }
29
75
  }
30
76
 
@@ -38,7 +84,8 @@
38
84
  }
39
85
 
40
86
  function getMaxInflight() {
41
- const base = isMobile() ? MAX_INFLIGHT_MOBILE : MAX_INFLIGHT_DESKTOP;
87
+ const perf = getPerfProfile();
88
+ const base = isMobile() ? perf.maxInflightMobile : perf.maxInflightDesktop;
42
89
  return base + (isBoosted() ? 1 : 0);
43
90
  }
44
91
 
@@ -48,6 +95,31 @@
48
95
  categoryItem: 'li[component="categories/category"]',
49
96
  };
50
97
 
98
+ const RELEVANT_MATCHERS = [
99
+ SELECTORS.postItem,
100
+ SELECTORS.topicItem,
101
+ SELECTORS.categoryItem,
102
+ ];
103
+
104
+ function mutationHasRelevantAddedNodes(mutations) {
105
+ try {
106
+ for (const m of mutations) {
107
+ if (!m || !m.addedNodes || !m.addedNodes.length) continue;
108
+ for (const n of m.addedNodes) {
109
+ if (!n || n.nodeType !== 1) continue;
110
+ const el = /** @type {Element} */ (n);
111
+ for (const sel of RELEVANT_MATCHERS) {
112
+ if (el.matches && el.matches(sel)) return true;
113
+ if (el.querySelector && el.querySelector(sel)) return true;
114
+ }
115
+ }
116
+ }
117
+ } catch (e) {}
118
+ return false;
119
+ }
120
+
121
+
122
+
51
123
  // Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
52
124
  let blockedUntil = 0;
53
125
  function isBlocked() {
@@ -95,29 +167,6 @@
95
167
  };
96
168
 
97
169
  const insertingIds = new Set();
98
-
99
-
100
- function markEmptyWrapper(id) {
101
- try {
102
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
103
- if (!ph || !ph.isConnected) return;
104
- const wrap = ph.closest ? ph.closest(`.${WRAP_CLASS}`) : null;
105
- if (!wrap) return;
106
- // If still empty after a delay, collapse it.
107
- setTimeout(() => {
108
- try {
109
- const ph2 = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
110
- if (!ph2 || !ph2.isConnected) return;
111
- const w2 = ph2.closest ? ph2.closest(`.${WRAP_CLASS}`) : null;
112
- if (!w2) return;
113
- // consider empty if only whitespace and no iframes/ins/img
114
- const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
115
- if (!hasAd) w2.classList.add('is-empty');
116
- } catch (e) {}
117
- }, 3500);
118
- } catch (e) {}
119
- }
120
-
121
170
  // Production build: debug disabled
122
171
  function dbg() {}
123
172
 
@@ -364,22 +413,6 @@ function withInternalDomChange(fn) {
364
413
  if (removed) dbg('prune-orphan', kindClass, { removed });
365
414
  return removed;
366
415
  }
367
-
368
- function refreshEmptyState(id) {
369
- // After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
370
- window.setTimeout(() => {
371
- try {
372
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
373
- if (!ph || !ph.isConnected) return;
374
- const wrap = ph.closest(`.${WRAP_CLASS}`);
375
- if (!wrap) return;
376
- const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
377
- if (hasContent) wrap.classList.remove('is-empty');
378
- else wrap.classList.add('is-empty');
379
- } catch (e) {}
380
- }, 3500);
381
- }
382
-
383
416
  function buildWrap(id, kindClass, afterPos) {
384
417
  const wrap = document.createElement('div');
385
418
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
@@ -450,13 +483,20 @@ function buildWrap(id, kindClass, afterPos) {
450
483
  // Otherwise remove the earliest one in the document
451
484
  if (!victim) victim = wraps[0];
452
485
 
453
- // Unobserve placeholder if still observed
454
- try {
455
- const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
456
- if (ph && state.io) state.io.unobserve(ph);
457
- } catch (e) {}
486
+ const id = getWrapIdFromWrap(victim);
487
+
488
+ withInternalDomChange(() => {
489
+ // Unobserve placeholder if still observed
490
+ try {
491
+ const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
492
+ if (ph && state.io) state.io.unobserve(ph);
493
+ } catch (e) {}
494
+
495
+ try { if (id) safeDestroyById(id); } catch (e) {}
496
+
497
+ try { victim.remove(); } catch (e) {}
498
+ });
458
499
 
459
- victim.remove();
460
500
  return true;
461
501
  } catch (e) {
462
502
  return false;
@@ -523,14 +563,13 @@ function startShow(id) {
523
563
 
524
564
  const doShow = () => {
525
565
  try {
526
- if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
527
- try { ez.destroyPlaceholders(id); } catch (e) {}
566
+ if (state.usedOnce && state.usedOnce.has(id)) {
567
+ safeDestroyById(id);
528
568
  }
529
569
  } catch (e) {}
530
570
 
531
571
  try { ez.showAds(id); } catch (e) {}
532
572
  try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
533
- try { markEmptyWrapper(id); } catch (e) {}
534
573
 
535
574
  setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
536
575
  };
@@ -618,7 +657,8 @@ function startShow(id) {
618
657
 
619
658
  const targets = computeTargets(items.length, interval, showFirst);
620
659
  let inserted = 0;
621
- const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
660
+ const perf = getPerfProfile();
661
+ const maxInserts = perf.maxInsertsPerRun + (isBoosted() ? 1 : 0);
622
662
 
623
663
  for (const afterPos of targets) {
624
664
  if (inserted >= maxInserts) break;
@@ -787,11 +827,16 @@ function startShow(id) {
787
827
 
788
828
  // remove all wrappers
789
829
  try {
790
- document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
791
- try { el.remove(); } catch (e) {}
830
+ withInternalDomChange(() => {
831
+ document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
832
+ try { el.remove(); } catch (e) {}
833
+ });
792
834
  });
793
835
  } catch (e) {}
794
836
 
837
+ // reset perf profile cache
838
+ state.perfProfile = null;
839
+
795
840
  // reset state
796
841
  state.cfg = null;
797
842
  state.allTopics = [];
@@ -812,9 +857,11 @@ function startShow(id) {
812
857
 
813
858
  function ensureDomObserver() {
814
859
  if (state.domObs) return;
815
- state.domObs = new MutationObserver(() => {
860
+ state.domObs = new MutationObserver((mutations) => {
816
861
  if (state.internalDomChange > 0) return;
817
- if (!isBlocked()) scheduleRun();
862
+ if (isBlocked()) return;
863
+ // Only rescan when NodeBB actually added posts/topics/categories.
864
+ if (mutationHasRelevantAddedNodes(mutations)) scheduleRun();
818
865
  });
819
866
  try {
820
867
  state.domObs.observe(document.body, { childList: true, subtree: true });
package/public/style.css CHANGED
@@ -1,40 +1,9 @@
1
- /* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
1
+ /* Minimal styling for injected Ezoic wrappers.
2
+ Spacing (margins/padding) is intentionally NOT forced here, because it can be
3
+ configured inside Ezoic and may vary by placement/device.
4
+ */
5
+
2
6
  .ezoic-ad {
3
7
  display: block;
4
8
  width: 100%;
5
- margin: 0 !important;
6
- padding: 0 !important;
7
- overflow: hidden;
8
- }
9
-
10
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
11
- margin: 0 !important;
12
- padding: 0 !important;
13
- min-height: 1px; /* keeps placeholder measurable for IO */
14
- }
15
-
16
- /* Ezoic sometimes wraps in extra spans/divs with margins */
17
- .ezoic-ad span.ezoic-ad,
18
- .ezoic-ad .ezoic-ad {
19
- margin: 0 !important;
20
- padding: 0 !important;
21
- }
22
-
23
-
24
- /* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
25
- .ezoic-ad.is-empty {
26
- display: block !important;
27
- margin: 0 !important;
28
- padding: 0 !important;
29
- height: 0 !important;
30
- min-height: 0 !important;
31
- overflow: hidden !important;
32
- }
33
-
34
- .ezoic-ad {
35
- min-height: 0 !important;
36
- }
37
-
38
- .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
39
- min-height: 0 !important;
40
9
  }