nodebb-plugin-ezoic-infinite 1.8.20 → 1.8.21

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 +135 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.20",
3
+ "version": "1.8.21",
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
@@ -22,6 +22,11 @@
22
22
  const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
23
23
  const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
24
24
  const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
25
+ const CLEANUP_GRACE_MS = 3_500; // délai mini avant cleanup d'un wrap candidat
26
+ const SPECIAL_GRACE_MS = 30_000; // délai allongé pour sticky/fixed/adhesion
27
+ const RECENT_WRAP_ACTIVITY_MS = 5_000; // protège un wrap récemment muté/rafraîchi
28
+ const VIEWPORT_BUFFER_DESKTOP = 500;
29
+ const VIEWPORT_BUFFER_MOBILE = 250;
25
30
 
26
31
  // Marges IO larges et fixes — observer créé une seule fois au boot
27
32
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
@@ -76,6 +81,8 @@
76
81
  wrapByKey: new Map(), // anchorKey → wrap DOM node
77
82
  ezActiveIds: new Set(), // ids actifs côté plugin (wrap présent / récemment show)
78
83
  ezShownSinceDestroy: new Set(), // ids déjà show depuis le dernier destroy Ezoic
84
+ wrapActivityAt: new Map(), // key/id -> ts activité DOM récente
85
+ lastWrapHeightByClass: new Map(),
79
86
  scrollDir: 1, // 1=bas, -1=haut
80
87
  scrollSpeed: 0, // px/s approx (EMA)
81
88
  lastScrollY: 0,
@@ -95,6 +102,102 @@
95
102
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
96
103
  const isFilled = n => !!(n?.querySelector?.(FILL_SEL));
97
104
 
105
+ function viewportBufferPx() {
106
+ return isMobile() ? VIEWPORT_BUFFER_MOBILE : VIEWPORT_BUFFER_DESKTOP;
107
+ }
108
+
109
+ function textSig(el) {
110
+ if (!(el instanceof Element)) return '';
111
+ const parts = [el.id || '', String(el.className || ''), el.getAttribute?.('name') || ''];
112
+ try {
113
+ for (const a of ['data-google-query-id', 'data-google-container-id', 'data-slot', 'data-ad-slot']) {
114
+ const v = el.getAttribute?.(a);
115
+ if (v) parts.push(v);
116
+ }
117
+ } catch (_) {}
118
+ return parts.join(' ').toLowerCase();
119
+ }
120
+
121
+ function isSpecialSlotLike(el) {
122
+ const sig = textSig(el);
123
+ return /adhesion|interstitial|anchor|sticky|outofpage/.test(sig);
124
+ }
125
+
126
+ function hasSpecialSlotMarkers(root) {
127
+ if (!(root instanceof Element)) return false;
128
+ if (isSpecialSlotLike(root)) return true;
129
+ try {
130
+ const nodes = root.querySelectorAll('[id],[class],[name],[data-slot],[data-ad-slot]');
131
+ for (const n of nodes) if (isSpecialSlotLike(n)) return true;
132
+ } catch (_) {}
133
+ return false;
134
+ }
135
+
136
+ function hasFixedLikeNode(root, maxScan = 24) {
137
+ if (!(root instanceof Element)) return false;
138
+ const q = [root];
139
+ let seen = 0;
140
+ while (q.length && seen < maxScan) {
141
+ const n = q.shift();
142
+ seen++;
143
+ try {
144
+ const cs = window.getComputedStyle(n);
145
+ if (cs.position === 'fixed' || cs.position === 'sticky') return true;
146
+ } catch (_) {}
147
+ for (const c of n.children || []) q.push(c);
148
+ }
149
+ return false;
150
+ }
151
+
152
+ function markWrapActivity(wrapOrId) {
153
+ const t = ts();
154
+ try {
155
+ if (wrapOrId instanceof Element) {
156
+ const key = wrapOrId.getAttribute(A_ANCHOR);
157
+ const id = parseInt(wrapOrId.getAttribute(A_WRAPID), 10);
158
+ if (key) S.wrapActivityAt.set(`k:${key}`, t);
159
+ if (Number.isFinite(id)) S.wrapActivityAt.set(`i:${id}`, t);
160
+ return;
161
+ }
162
+ const id = parseInt(wrapOrId, 10);
163
+ if (Number.isFinite(id)) S.wrapActivityAt.set(`i:${id}`, t);
164
+ } catch (_) {}
165
+ }
166
+
167
+ function wrapRecentActivity(w) {
168
+ try {
169
+ const key = w?.getAttribute?.(A_ANCHOR);
170
+ const id = parseInt(w?.getAttribute?.(A_WRAPID), 10);
171
+ const t1 = key ? (S.wrapActivityAt.get(`k:${key}`) || 0) : 0;
172
+ const t2 = Number.isFinite(id) ? (S.wrapActivityAt.get(`i:${id}`) || 0) : 0;
173
+ const t3 = parseInt(w?.getAttribute?.(A_SHOWN) || '0', 10) || 0;
174
+ return (ts() - Math.max(t1, t2, t3)) < RECENT_WRAP_ACTIVITY_MS;
175
+ } catch (_) { return false; }
176
+ }
177
+
178
+ function wrapCleanupGraceMs(w) {
179
+ return (hasSpecialSlotMarkers(w) || hasFixedLikeNode(w)) ? SPECIAL_GRACE_MS : CLEANUP_GRACE_MS;
180
+ }
181
+
182
+ function wrapNearViewport(w) {
183
+ try {
184
+ const r = w.getBoundingClientRect();
185
+ const b = viewportBufferPx();
186
+ const vh = window.innerHeight || 800;
187
+ return r.bottom > -b && r.top < vh + b;
188
+ } catch (_) { return true; }
189
+ }
190
+
191
+ function rememberWrapHeight(w) {
192
+ try {
193
+ if (!(w instanceof Element)) return;
194
+ const klass = [...(w.classList || [])].find(c => c.startsWith('ezoic-ad-'));
195
+ if (!klass) return;
196
+ const h = Math.round(w.getBoundingClientRect().height || 0);
197
+ if (h >= 40) S.lastWrapHeightByClass.set(klass, h);
198
+ } catch (_) {}
199
+ }
200
+
98
201
  function healFalseEmpty(root = document) {
99
202
  try {
100
203
  const list = [];
@@ -200,10 +303,20 @@ function destroyBeforeReuse(ids) {
200
303
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
201
304
  seen.add(id);
202
305
  out.push(id);
306
+ try {
307
+ const wrap = phEl(id)?.closest?.(WRAP_SEL) || null;
308
+ if (wrap && (wrapRecentActivity(wrap) || hasSpecialSlotMarkers(wrap) || hasFixedLikeNode(wrap))) {
309
+ continue;
310
+ }
311
+ } catch (_) {}
203
312
  if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
204
313
  }
205
314
  if (toDestroy.length) {
206
- try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
315
+ try {
316
+ const ez = window.ezstandalone;
317
+ const run = () => { try { ez?.destroyPlaceholders?.(toDestroy); } catch (_) {} };
318
+ try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
319
+ } catch (_) {}
207
320
  for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
208
321
  }
209
322
  return out;
@@ -408,6 +521,8 @@ function recycleAndMove(klass, targetEl, newKey) {
408
521
 
409
522
  for (const wrap of S.wrapByKey.values()) {
410
523
  if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
524
+ if (wrapRecentActivity(wrap)) continue;
525
+ if (hasSpecialSlotMarkers(wrap) || hasFixedLikeNode(wrap)) continue;
411
526
  try {
412
527
  const rect = wrap.getBoundingClientRect();
413
528
  const isAbove = rect.bottom <= farAbove;
@@ -442,6 +557,7 @@ function recycleAndMove(klass, targetEl, newKey) {
442
557
  const oldKey = best.getAttribute(A_ANCHOR);
443
558
  try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
444
559
  mutate(() => {
560
+ rememberWrapHeight(best);
445
561
  best.setAttribute(A_ANCHOR, newKey);
446
562
  best.setAttribute(A_CREATED, String(ts()));
447
563
  best.setAttribute(A_SHOWN, '0');
@@ -478,6 +594,8 @@ function recycleAndMove(klass, targetEl, newKey) {
478
594
  w.setAttribute(A_CREATED, String(ts()));
479
595
  w.setAttribute(A_SHOWN, '0');
480
596
  w.style.cssText = 'width:100%;display:block;';
597
+ const cachedH = S.lastWrapHeightByClass.get(klass);
598
+ if (Number.isFinite(cachedH) && cachedH > 0) w.style.minHeight = `${cachedH}px`;
481
599
  const ph = document.createElement('div');
482
600
  ph.id = `${PH_PREFIX}${id}`;
483
601
  ph.setAttribute('data-ezoic-id', String(id));
@@ -499,11 +617,14 @@ function recycleAndMove(klass, targetEl, newKey) {
499
617
 
500
618
  function dropWrap(w) {
501
619
  try {
620
+ rememberWrapHeight(w);
502
621
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
503
622
  if (ph instanceof Element) S.io?.unobserve(ph);
504
623
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
505
624
  if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
506
625
  const key = w.getAttribute(A_ANCHOR);
626
+ if (key) S.wrapActivityAt.delete(`k:${key}`);
627
+ if (Number.isFinite(id)) S.wrapActivityAt.delete(`i:${id}`);
507
628
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
508
629
  w.remove();
509
630
  } catch (_) {}
@@ -530,7 +651,8 @@ function recycleAndMove(klass, targetEl, newKey) {
530
651
 
531
652
  document.querySelectorAll(`${WRAP_SEL}.${klass}`).forEach(w => {
532
653
  const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
533
- if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
654
+ if (ts() - created < Math.max(MIN_PRUNE_AGE_MS, wrapCleanupGraceMs(w))) return;
655
+ if (wrapRecentActivity(w) || wrapNearViewport(w)) return;
534
656
 
535
657
  const key = w.getAttribute(A_ANCHOR) ?? '';
536
658
  const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
@@ -687,7 +809,7 @@ function startShowBatch(ids) {
687
809
  if (!canShowPlaceholderId(id, t)) continue;
688
810
 
689
811
  S.lastShow.set(id, t);
690
- try { ph.closest?.(WRAP_SEL)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
812
+ try { const wrap = ph.closest?.(WRAP_SEL); wrap?.setAttribute(A_SHOWN, String(t)); if (wrap) markWrapActivity(wrap); } catch (_) {}
691
813
  valid.push(id);
692
814
  }
693
815
 
@@ -844,6 +966,7 @@ function startShowBatch(ids) {
844
966
  S.wrapByKey.clear();
845
967
  S.ezActiveIds.clear();
846
968
  S.ezShownSinceDestroy.clear();
969
+ S.wrapActivityAt.clear();
847
970
  S.inflight = 0;
848
971
  S.pending = [];
849
972
  S.pendingSet.clear();
@@ -887,12 +1010,21 @@ function startShowBatch(ids) {
887
1010
  if (n.nodeType !== 1) continue;
888
1011
  if (nodeMatchesAny(n, [WRAP_SEL]) || nodeContainsAny(n, [WRAP_SEL])) {
889
1012
  sawWrapRemoval = true;
1013
+ try {
1014
+ if (n instanceof Element && n.classList?.contains(WRAP_CLASS)) markWrapActivity(n);
1015
+ else if (n instanceof Element) { const inner = n.querySelector?.(WRAP_SEL); if (inner) markWrapActivity(inner); }
1016
+ } catch (_) {}
890
1017
  }
891
1018
  }
892
1019
  if (sawWrapRemoval) queueSweepDeadWraps();
893
1020
  for (const n of m.addedNodes) {
894
1021
  if (n.nodeType !== 1) continue;
895
1022
  try { healFalseEmpty(n); } catch (_) {}
1023
+ try {
1024
+ const w = (n instanceof Element && (n.classList?.contains(WRAP_CLASS) ? n : n.closest?.(WRAP_SEL))) || null;
1025
+ if (w) markWrapActivity(w);
1026
+ else if (n instanceof Element) { const inner = n.querySelector?.(WRAP_SEL); if (inner) markWrapActivity(inner); }
1027
+ } catch (_) {}
896
1028
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
897
1029
  if (nodeMatchesAny(n, CONTENT_SEL_LIST) || nodeContainsAny(n, CONTENT_SEL_LIST)) {
898
1030
  requestBurst(); return;