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 +1 -1
- package/public/client.js +102 -55
- package/public/style.css +5 -36
package/package.json
CHANGED
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
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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)
|
|
527
|
-
|
|
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
|
|
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
|
-
|
|
791
|
-
|
|
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 (
|
|
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
|
-
/*
|
|
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
|
}
|