nodebb-plugin-ezoic-infinite 1.7.86 → 1.7.87

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.7.86",
3
+ "version": "1.7.87",
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
@@ -76,14 +76,10 @@
76
76
  const A_WRAPID = 'data-ezoic-wrapid'; // id Ezoic
77
77
  const A_CREATED = 'data-ezoic-created'; // timestamp création ms
78
78
  const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
79
- const A_FILLED = 'data-ezoic-filled'; // timestamp premier fill réel
80
- const A_LAST_H = 'data-ezoic-lasth'; // dernière hauteur pub vue
81
-
82
- const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
83
- const MIN_LIVE_AFTER_SHOW_MS = 15_000;
84
- const MIN_LIVE_AFTER_FILL_MS = 25_000;
85
- const KEEP_SHELL_AFTER_UNUSED_MS = 90_000;
86
- const MIN_SHELL_HEIGHT = 120;
79
+
80
+ // anti-race fill async (Ezoic peut remplir bien après showAds)
81
+ const EMPTY_CHECK_PASSES = [20_000, 25_000, 35_000];
82
+
87
83
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
88
84
  const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
89
85
  const MAX_INFLIGHT = 4; // max showAds() simultanés
@@ -126,6 +122,8 @@
126
122
  cursors: { topics: 0, posts: 0, categories: 0 },
127
123
  mountedIds: new Set(),
128
124
  lastShow: new Map(),
125
+ emptyChecks: new Map(), // id -> [timerIds] checks is-empty multi-pass
126
+ fillObs: new Map(), // id -> MutationObserver placeholder fill tardif
129
127
  io: null,
130
128
  domObs: null,
131
129
  mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
@@ -133,8 +131,6 @@
133
131
  pending: [], // ids en attente de slot inflight
134
132
  pendingSet: new Set(),
135
133
  wrapByKey: new Map(), // anchorKey → wrap DOM node
136
- emptyChecks: new Map(), // id → [timerIds]
137
- fillObsById: new Map(), // id → MutationObserver
138
134
  runQueued: false,
139
135
  burstActive: false,
140
136
  burstDeadline: 0,
@@ -150,85 +146,47 @@
150
146
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
151
147
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
152
148
 
149
+ function clearEmptyChecks(id) {
150
+ const arr = S.emptyChecks.get(id);
151
+ if (arr) {
152
+ for (const t of arr) clearTimeout(t);
153
+ S.emptyChecks.delete(id);
154
+ }
155
+ }
153
156
 
154
- function clearEmptyChecks(id) {
155
- const timers = S.emptyChecks.get(id);
156
- if (!timers) return;
157
- for (const t of timers) { try { clearTimeout(t); } catch (_) {} }
158
- S.emptyChecks.delete(id);
159
- }
160
- function queueEmptyCheck(id, timerId) {
161
- const arr = S.emptyChecks.get(id) || [];
162
- arr.push(timerId);
163
- S.emptyChecks.set(id, arr);
164
- }
165
- function markFilledOnce(ph) {
166
- try {
167
- const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
168
- if (!wrap || !isFilled(ph)) return;
169
- if (!wrap.getAttribute(A_FILLED)) wrap.setAttribute(A_FILLED, String(ts()));
170
- const h = Math.round(ph.getBoundingClientRect?.().height || ph.offsetHeight || 0);
171
- if (h > 0) wrap.setAttribute(A_LAST_H, String(h));
172
- } catch (_) {}
173
- }
174
- function shouldKeepShellAfterUnused(wrap) {
175
- try {
176
- const filledTs = parseInt(wrap?.getAttribute?.(A_FILLED) || '0', 10) || 0;
177
- return !!filledTs && (ts() - filledTs) < KEEP_SHELL_AFTER_UNUSED_MS;
178
- } catch (_) { return false; }
179
- }
180
- function applyUnusedShell(wrap, ph) {
181
- try {
182
- const lastH = parseInt(wrap.getAttribute(A_LAST_H) || '0', 10) || 0;
183
- const h = Math.max(MIN_SHELL_HEIGHT, lastH || 0);
184
- wrap.classList.remove('is-empty');
185
- wrap.classList.add('is-unused-shell');
186
- wrap.style.minHeight = `${h}px`;
187
- if (ph) ph.style.minHeight = `${h}px`;
188
- } catch (_) {}
189
- }
190
- function clearUnusedShell(wrap, ph) {
191
- try {
192
- wrap?.classList?.remove('is-unused-shell');
193
- wrap?.style?.removeProperty('min-height');
194
- ph?.style?.removeProperty('min-height');
195
- } catch (_) {}
196
- }
197
- function isProtectedFromDrop(wrap) {
198
- try {
199
- const now = ts();
200
- const shownTs = parseInt(wrap?.getAttribute?.(A_SHOWN) || '0', 10) || 0;
201
- const filledTs = parseInt(wrap?.getAttribute?.(A_FILLED) || '0', 10) || 0;
202
- if (shownTs && (now - shownTs) < MIN_LIVE_AFTER_SHOW_MS) return true;
203
- if (filledTs && (now - filledTs) < MIN_LIVE_AFTER_FILL_MS) return true;
204
- return false;
205
- } catch (_) { return false; }
206
- }
207
- function uncollapseIfFilled(ph) {
208
- try {
209
- const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
210
- if (!wrap) return;
211
- if (isFilled(ph)) {
212
- wrap.classList.remove('is-empty');
213
- clearUnusedShell(wrap, ph);
214
- markFilledOnce(ph);
157
+ function queueEmptyCheck(id, timerId) {
158
+ const arr = S.emptyChecks.get(id) || [];
159
+ arr.push(timerId);
160
+ S.emptyChecks.set(id, arr);
161
+ }
162
+
163
+ function uncollapseIfFilled(ph) {
164
+ try {
165
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
166
+ if (!wrap) return;
167
+ if (isFilled(ph)) wrap.classList.remove('is-empty');
168
+ } catch (_) {}
169
+ }
170
+
171
+ function watchPlaceholderFill(id) {
172
+ try {
173
+ const ph = document.getElementById(`${PH_PREFIX}${id}`);
174
+ if (!ph?.isConnected) return;
175
+ if (S.fillObs.has(id)) return;
176
+ const obs = new MutationObserver(() => uncollapseIfFilled(ph));
177
+ obs.observe(ph, { childList: true, subtree: true, attributes: true });
178
+ S.fillObs.set(id, obs);
179
+ uncollapseIfFilled(ph);
180
+ } catch (_) {}
181
+ }
182
+
183
+ function unwatchPlaceholderFill(id) {
184
+ const obs = S.fillObs.get(id);
185
+ if (obs) {
186
+ try { obs.disconnect(); } catch (_) {}
187
+ S.fillObs.delete(id);
215
188
  }
216
- } catch (_) {}
217
- }
218
- function unwatchPlaceholderFill(id) {
219
- const obs = S.fillObsById.get(id);
220
- if (obs) { try { obs.disconnect(); } catch (_) {} S.fillObsById.delete(id); }
221
- }
222
- function watchPlaceholderFill(id) {
223
- try {
224
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
225
- if (!ph || !ph.isConnected || S.fillObsById.has(id)) return;
226
- const obs = new MutationObserver(() => uncollapseIfFilled(ph));
227
- obs.observe(ph, { childList: true, subtree: true, attributes: true });
228
- S.fillObsById.set(id, obs);
229
- uncollapseIfFilled(ph);
230
- } catch (_) {}
231
- }
189
+ }
232
190
 
233
191
  function mutate(fn) {
234
192
  S.mutGuard++;
@@ -429,20 +387,19 @@ function watchPlaceholderFill(id) {
429
387
  // Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
430
388
  // parasite si le nœud était encore dans la zone IO_MARGIN.
431
389
  try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
390
+ if (Number.isFinite(id)) clearEmptyChecks(id);
432
391
  mutate(() => {
433
392
  best.setAttribute(A_ANCHOR, newKey);
434
393
  best.setAttribute(A_CREATED, String(ts()));
435
394
  best.setAttribute(A_SHOWN, '0');
436
395
  best.classList.remove('is-empty');
437
- best.classList.remove('is-unused-shell');
438
- best.removeAttribute(A_FILLED);
439
- best.removeAttribute(A_LAST_H);
440
396
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
441
- if (ph) { ph.innerHTML = ''; ph.style.removeProperty('min-height'); }
397
+ if (ph) ph.innerHTML = '';
442
398
  targetEl.insertAdjacentElement('afterend', best);
443
399
  });
444
400
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
445
401
  S.wrapByKey.set(newKey, best);
402
+ observePh(id);
446
403
 
447
404
  // Délais requis : destroyPlaceholders est asynchrone en interne
448
405
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
@@ -479,24 +436,16 @@ function watchPlaceholderFill(id) {
479
436
  mutate(() => el.insertAdjacentElement('afterend', w));
480
437
  S.mountedIds.add(id);
481
438
  S.wrapByKey.set(key, w);
482
- watchPlaceholderFill(id);
483
439
  return w;
484
440
  }
485
441
 
486
442
  function dropWrap(w) {
487
443
  try {
488
444
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
489
- const id = parseInt(w.getAttribute(A_WRAPID), 10);
490
- if (isProtectedFromDrop(w) || shouldKeepShellAfterUnused(w)) {
491
- if (ph) applyUnusedShell(w, ph);
492
- return;
493
- }
494
445
  if (ph instanceof Element) S.io?.unobserve(ph);
495
- if (Number.isFinite(id)) {
496
- clearEmptyChecks(id);
497
- unwatchPlaceholderFill(id);
498
- S.mountedIds.delete(id);
499
- }
446
+ const id = parseInt(w.getAttribute(A_WRAPID), 10);
447
+ if (Number.isFinite(id)) { clearEmptyChecks(id); unwatchPlaceholderFill(id); }
448
+ if (Number.isFinite(id)) S.mountedIds.delete(id);
500
449
  const key = w.getAttribute(A_ANCHOR);
501
450
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
502
451
  w.remove();
@@ -604,8 +553,8 @@ function watchPlaceholderFill(id) {
604
553
  function observePh(id) {
605
554
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
606
555
  if (ph?.isConnected) {
607
- watchPlaceholderFill(id);
608
556
  try { getIO()?.observe(ph); } catch (_) {}
557
+ watchPlaceholderFill(id);
609
558
  }
610
559
  }
611
560
 
@@ -645,8 +594,9 @@ function watchPlaceholderFill(id) {
645
594
  if (isBlocked()) { clearTimeout(timer); return release(); }
646
595
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
647
596
  if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
597
+
648
598
  clearEmptyChecks(id);
649
- try { const wrap = ph.closest?.(`.${WRAP_CLASS}`); wrap?.classList?.remove('is-empty'); clearUnusedShell(wrap, ph); } catch (_) {}
599
+ try { ph.closest?.(`.${WRAP_CLASS}`)?.classList.remove('is-empty'); } catch (_) {}
650
600
 
651
601
  const t = ts();
652
602
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
@@ -667,35 +617,24 @@ function watchPlaceholderFill(id) {
667
617
  }
668
618
 
669
619
  function scheduleEmptyCheck(id, showTs) {
620
+ clearEmptyChecks(id);
621
+
670
622
  const runCheck = () => {
671
623
  try {
672
624
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
673
625
  const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
674
626
  if (!wrap || !ph?.isConnected) return;
675
627
  if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
676
-
677
- if (isFilled(ph)) {
678
- wrap.classList.remove('is-empty');
679
- clearUnusedShell(wrap, ph);
680
- markFilledOnce(ph);
681
- return;
682
- }
683
-
684
- if (shouldKeepShellAfterUnused(wrap)) {
685
- applyUnusedShell(wrap, ph);
686
- return;
687
- }
688
-
689
- clearUnusedShell(wrap, ph);
690
- wrap.classList.add('is-empty');
628
+ const filled = isFilled(ph);
629
+ if (filled) wrap.classList.remove('is-empty');
630
+ else wrap.classList.add('is-empty');
691
631
  } catch (_) {}
692
632
  };
693
633
 
694
- clearEmptyChecks(id);
695
- [EMPTY_CHECK_MS, EMPTY_CHECK_MS + 5000, EMPTY_CHECK_MS + 15000].forEach(delay => {
696
- const t = setTimeout(runCheck, delay);
697
- queueEmptyCheck(id, t);
698
- });
634
+ for (const delay of EMPTY_CHECK_PASSES) {
635
+ const tid = setTimeout(runCheck, delay);
636
+ queueEmptyCheck(id, tid);
637
+ }
699
638
  }
700
639
 
701
640
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
@@ -821,12 +760,14 @@ function watchPlaceholderFill(id) {
821
760
  S.cursors = { topics: 0, posts: 0, categories: 0 };
822
761
  S.mountedIds.clear();
823
762
  S.lastShow.clear();
824
- for (const id of Array.from(S.emptyChecks.keys())) clearEmptyChecks(id);
825
- for (const id of Array.from(S.fillObsById.keys())) unwatchPlaceholderFill(id);
826
763
  S.wrapByKey.clear();
827
764
  S.inflight = 0;
828
765
  S.pending = [];
829
766
  S.pendingSet.clear();
767
+ S.emptyChecks.forEach(arr => { try { arr.forEach(clearTimeout); } catch (_) {} });
768
+ S.emptyChecks.clear();
769
+ S.fillObs.forEach(obs => { try { obs.disconnect(); } catch (_) {} });
770
+ S.fillObs.clear();
830
771
  S.burstActive = false;
831
772
  S.runQueued = false;
832
773
  }
package/public/style.css CHANGED
@@ -8,7 +8,7 @@
8
8
  width: 100%;
9
9
  margin: 0 !important;
10
10
  padding: 0 !important;
11
- overflow: visible;
11
+ overflow: hidden;
12
12
  contain: layout style;
13
13
  }
14
14
 
@@ -71,14 +71,7 @@
71
71
  overflow: hidden !important;
72
72
  }
73
73
 
74
- /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
75
- .ezoic-ad {
76
- margin: 0 !important;
77
- padding: 0 !important;
78
- }
79
-
80
-
81
- /* Filet de sécurité : si un fill arrive tard alors que .is-empty est resté, ne pas écraser */
74
+ /* Filet de sécurité : si un fill est présent malgré is-empty, on ne collapse pas */
82
75
  .nodebb-ezoic-wrap.is-empty:has(iframe, ins, img, video, [data-google-container-id]) {
83
76
  height: auto !important;
84
77
  min-height: 1px !important;
@@ -86,30 +79,8 @@
86
79
  overflow: visible !important;
87
80
  }
88
81
 
89
- /* Shell conservé quand Ezoic repasse en "unused" après un affichage initial */
90
- .nodebb-ezoic-wrap.is-unused-shell {
91
- overflow: hidden !important;
92
- }
93
- .nodebb-ezoic-wrap.is-unused-shell > [id^="ezoic-pub-ad-placeholder-"] {
94
- display: block;
95
- width: 100%;
96
- }
97
-
98
- /* Responsive hardening conservatif (sans scale JS) */
99
- .nodebb-ezoic-wrap,
100
- .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
101
- max-width: 100%;
102
- }
103
- .nodebb-ezoic-wrap .ezoic-ad,
104
- .nodebb-ezoic-wrap span.ezoic-ad {
105
- max-width: 100% !important;
106
- min-width: 0 !important;
107
- box-sizing: border-box !important;
108
- }
109
-
110
- /* Neutralisation sticky plus défensive dans les wraps du plugin */
111
- .nodebb-ezoic-wrap .ezads-sticky-intradiv,
112
- .nodebb-ezoic-wrap [style*="position: sticky"] {
113
- position: static !important;
114
- top: auto !important;
82
+ /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
83
+ .ezoic-ad {
84
+ margin: 0 !important;
85
+ padding: 0 !important;
115
86
  }