nodebb-plugin-ezoic-infinite 1.7.93 → 1.7.95

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 +75 -146
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.93",
3
+ "version": "1.7.95",
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
@@ -77,12 +77,11 @@
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
79
 
80
+ const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
80
81
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
81
82
  const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
82
- const MAX_INFLIGHT = 2; // max showAds() simultanés (réduit le churn/unused)
83
- const SHOW_THROTTLE_MS = 1200; // anti-spam showAds() par id
84
- const UNUSED_COOLDOWN_MS = 15_000; // cooldown après tentative sans fill (unused probable)
85
- const PENDING_TIMEOUT_MS = 12_000; // au-delà, tentative considérée obsolète
83
+ const MAX_INFLIGHT = 4; // max showAds() simultanés
84
+ const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
86
85
  const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
87
86
 
88
87
  // Marges IO larges et fixes — observer créé une seule fois au boot
@@ -125,11 +124,8 @@
125
124
  domObs: null,
126
125
  mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
127
126
  inflight: 0, // showAds() en cours
128
- pending: [], // ids en attente de slot inflight (triés par priorité)
127
+ pending: [], // ids en attente de slot inflight
129
128
  pendingSet: new Set(),
130
- pendingShows: new Set(), // ids avec tentative show en cours
131
- cooldownUntil:new Map(), // id -> ts de fin de cooldown (unused probable)
132
- fillObsById: new Map(), // id -> MutationObserver placeholder fill
133
129
  wrapByKey: new Map(), // anchorKey → wrap DOM node
134
130
  runQueued: false,
135
131
  burstActive: false,
@@ -137,7 +133,7 @@
137
133
  burstCount: 0,
138
134
  lastBurstTs: 0,
139
135
  lastScrollY: 0,
140
- scrollDir: 1,
136
+ scrollDir: 1, // 1=down, -1=up
141
137
  };
142
138
 
143
139
  let blockedUntil = 0;
@@ -147,69 +143,6 @@
147
143
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
148
144
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
149
145
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
150
- const isCooling = id => (S.cooldownUntil.get(id) || 0) > ts();
151
-
152
- function viewportDistance(el) {
153
- try {
154
- const r = el?.getBoundingClientRect?.();
155
- if (!r) return Infinity;
156
- const vh = window.innerHeight || 800;
157
- if (r.bottom >= 0 && r.top <= vh) return 0;
158
- if (r.top > vh) return Math.max(0, r.top - vh);
159
- return Math.max(0, -r.bottom);
160
- } catch (_) { return Infinity; }
161
- }
162
-
163
- function pendingEnqueue(id) {
164
- if (S.pendingSet.has(id)) return;
165
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
166
- const prio = viewportDistance(ph?.closest?.(`.${WRAP_CLASS}`) || ph);
167
- const item = { id, prio, at: ts() };
168
- let i = S.pending.length;
169
- while (i > 0 && S.pending[i - 1].prio > prio) i--;
170
- S.pending.splice(i, 0, item);
171
- S.pendingSet.add(id);
172
- }
173
-
174
- function markUnusedProbable(id) {
175
- if (!id) return;
176
- S.pendingShows.delete(id);
177
- S.cooldownUntil.set(id, ts() + UNUSED_COOLDOWN_MS);
178
- }
179
-
180
- function clearCooldownIfFilled(id) {
181
- if (!id) return;
182
- S.cooldownUntil.delete(id);
183
- S.pendingShows.delete(id);
184
- }
185
-
186
- function unobserveFill(id) {
187
- const obs = S.fillObsById.get(id);
188
- if (obs) { try { obs.disconnect(); } catch (_) {} }
189
- S.fillObsById.delete(id);
190
- }
191
-
192
- function watchPlaceholderFill(id) {
193
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
194
- if (!ph?.isConnected) return;
195
- unobserveFill(id);
196
- const onMut = () => {
197
- try {
198
- if (!ph.isConnected) return unobserveFill(id);
199
- if (!isFilled(ph)) return;
200
- clearCooldownIfFilled(id);
201
- const wrap = ph.closest?.(`.${WRAP_CLASS}`);
202
- if (wrap) wrap.classList.remove('is-empty');
203
- } catch (_) {}
204
- };
205
- try {
206
- const obs = new MutationObserver(onMut);
207
- obs.observe(ph, { childList: true, subtree: true, attributes: true });
208
- S.fillObsById.set(id, obs);
209
- } catch (_) {}
210
- onMut();
211
- }
212
-
213
146
 
214
147
  function mutate(fn) {
215
148
  S.mutGuard++;
@@ -351,21 +284,6 @@
351
284
  return (w?.isConnected) ? w : null;
352
285
  }
353
286
 
354
- function sweepDeadWraps() {
355
- for (const [key, w] of Array.from(S.wrapByKey.entries())) {
356
- if (w?.isConnected) continue;
357
- S.wrapByKey.delete(key);
358
- const id = parseInt(w?.getAttribute?.(A_WRAPID), 10);
359
- if (Number.isFinite(id)) {
360
- S.mountedIds.delete(id);
361
- S.pendingSet.delete(id);
362
- S.pending = S.pending.filter(it => (it?.id ?? it) !== id);
363
- S.pendingShows.delete(id);
364
- unobserveFill(id);
365
- }
366
- }
367
- }
368
-
369
287
  // ── Pool ───────────────────────────────────────────────────────────────────
370
288
 
371
289
  /**
@@ -398,39 +316,56 @@
398
316
  typeof ez?.displayMore !== 'function') return null;
399
317
 
400
318
  const vh = window.innerHeight || 800;
401
- const targetRect = targetEl?.getBoundingClientRect?.();
402
- const dir = S.scrollDir >= 0 ? 1 : -1;
319
+ const targetRect = targetEl?.getBoundingClientRect?.() || { top: vh, bottom: vh };
320
+
321
+ // Recyclage bidirectionnel :
322
+ // - scroll vers le bas -> recycle préférentiellement des wraps loin au-dessus
323
+ // - scroll vers le haut -> recycle préférentiellement des wraps loin en-dessous
324
+ // Fallback sur l'autre côté si aucun candidat.
325
+ const aboveThreshold = -vh; // wrap entièrement/suffisamment au-dessus
326
+ const belowThreshold = vh * 2; // wrap loin sous le viewport
403
327
 
404
- let best = null, bestScore = Infinity;
405
- let fallback = null, fallbackScore = Infinity;
328
+ let aboveEmpty = null, aboveEmptyBottom = Infinity;
329
+ let aboveFilled = null, aboveFilledBottom = Infinity;
330
+ let belowEmpty = null, belowEmptyTop = -Infinity;
331
+ let belowFilled = null, belowFilledTop = -Infinity;
406
332
 
407
- for (const wrap of S.wrapByKey.values()) {
333
+ document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
408
334
  try {
409
- if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
335
+ if (!wrap?.isConnected) return;
410
336
  const rect = wrap.getBoundingClientRect();
411
- const farAbove = rect.bottom < -vh;
412
- const farBelow = rect.top > (window.innerHeight || 800) + vh;
413
- if (!farAbove && !farBelow) continue;
414
-
415
- const preferredSide = dir > 0 ? farAbove : farBelow;
416
- const dist = targetRect ? Math.abs(rect.top - targetRect.top) : Math.abs(rect.top);
417
- const score = (isFilled(wrap) ? 10000000 : 0) + dist;
418
-
419
- if (preferredSide) {
420
- if (score < bestScore) { best = wrap; bestScore = score; }
421
- } else if (score < fallbackScore) {
422
- fallback = wrap; fallbackScore = score;
337
+
338
+ if (rect.bottom < aboveThreshold) {
339
+ if (!isFilled(wrap)) {
340
+ if (rect.bottom < aboveEmptyBottom) { aboveEmptyBottom = rect.bottom; aboveEmpty = wrap; }
341
+ } else {
342
+ if (rect.bottom < aboveFilledBottom) { aboveFilledBottom = rect.bottom; aboveFilled = wrap; }
343
+ }
344
+ return;
345
+ }
346
+
347
+ if (rect.top > belowThreshold) {
348
+ if (!isFilled(wrap)) {
349
+ if (rect.top > belowEmptyTop) { belowEmptyTop = rect.top; belowEmpty = wrap; }
350
+ } else {
351
+ if (rect.top > belowFilledTop) { belowFilledTop = rect.top; belowFilled = wrap; }
352
+ }
423
353
  }
424
354
  } catch (_) {}
425
- }
355
+ });
426
356
 
427
- best = best || fallback;
357
+ const preferBelow = (S.scrollDir < 0) || (targetRect.top < vh * 0.5);
358
+ const pickAbove = () => aboveEmpty ?? aboveFilled;
359
+ const pickBelow = () => belowEmpty ?? belowFilled;
360
+ const best = preferBelow ? (pickBelow() ?? pickAbove()) : (pickAbove() ?? pickBelow());
428
361
  if (!best) return null;
429
362
  const id = parseInt(best.getAttribute(A_WRAPID), 10);
430
363
  if (!Number.isFinite(id)) return null;
431
364
 
432
365
  const oldKey = best.getAttribute(A_ANCHOR);
433
- try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { S.io?.unobserve(ph); unobserveFill(id); } } catch (_) {}
366
+ // Neutraliser l'IO sur ce wrap avant déplacement évite un showAds
367
+ // parasite si le nœud était encore dans la zone IO_MARGIN.
368
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
434
369
  mutate(() => {
435
370
  best.setAttribute(A_ANCHOR, newKey);
436
371
  best.setAttribute(A_CREATED, String(ts()));
@@ -443,9 +378,10 @@
443
378
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
444
379
  S.wrapByKey.set(newKey, best);
445
380
 
446
- const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
447
- const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
448
- const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
381
+ // Délais requis : destroyPlaceholders est asynchrone en interne
382
+ const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
383
+ const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
384
+ const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
449
385
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
450
386
 
451
387
  return { id, wrap: best };
@@ -485,7 +421,6 @@
485
421
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
486
422
  if (ph instanceof Element) S.io?.unobserve(ph);
487
423
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
488
- if (Number.isFinite(id)) unobserveFill(id);
489
424
  if (Number.isFinite(id)) S.mountedIds.delete(id);
490
425
  const key = w.getAttribute(A_ANCHOR);
491
426
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
@@ -549,7 +484,6 @@
549
484
  function injectBetween(klass, items, interval, showFirst, poolKey) {
550
485
  if (!items.length) return 0;
551
486
  let inserted = 0;
552
- sweepDeadWraps();
553
487
 
554
488
  for (const el of items) {
555
489
  if (inserted >= MAX_INSERTS_RUN) break;
@@ -562,15 +496,13 @@
562
496
  const key = anchorKey(klass, el);
563
497
  if (findWrap(key)) continue;
564
498
 
565
- let id = pickId(poolKey);
566
- if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
499
+ const id = pickId(poolKey);
567
500
  if (id) {
568
501
  const w = insertAfter(el, id, klass, key);
569
502
  if (w) { observePh(id); inserted++; }
570
503
  } else {
571
504
  const recycled = recycleAndMove(klass, el, key);
572
505
  if (!recycled) break;
573
- observePh(recycled.id);
574
506
  inserted++;
575
507
  }
576
508
  }
@@ -596,54 +528,45 @@
596
528
 
597
529
  function observePh(id) {
598
530
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
599
- if (ph?.isConnected) {
600
- try { getIO()?.observe(ph); } catch (_) {}
601
- watchPlaceholderFill(id);
602
- }
531
+ if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
603
532
  }
604
533
 
605
534
  function enqueueShow(id) {
606
535
  if (!id || isBlocked()) return;
607
- if (isCooling(id) || S.pendingShows.has(id)) return;
608
536
  if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
609
- if (S.inflight >= MAX_INFLIGHT) { pendingEnqueue(id); return; }
537
+ if (S.inflight >= MAX_INFLIGHT) {
538
+ if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
539
+ return;
540
+ }
610
541
  startShow(id);
611
542
  }
612
543
 
613
544
  function drainQueue() {
614
545
  if (isBlocked()) return;
615
546
  while (S.inflight < MAX_INFLIGHT && S.pending.length) {
616
- const item = S.pending.shift();
617
- const id = item?.id ?? item;
547
+ const id = S.pending.shift();
618
548
  S.pendingSet.delete(id);
619
- if (!id || isCooling(id) || S.pendingShows.has(id)) continue;
620
- if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
621
- if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) continue;
622
549
  startShow(id);
623
550
  }
624
551
  }
625
552
 
626
553
  function startShow(id) {
627
554
  if (!id || isBlocked()) return;
628
- if (S.pendingShows.has(id) || isCooling(id)) return;
629
- S.pendingShows.add(id);
630
555
  S.inflight++;
631
556
  let done = false;
632
557
  const release = () => {
633
558
  if (done) return;
634
559
  done = true;
635
- S.pendingShows.delete(id);
636
560
  S.inflight = Math.max(0, S.inflight - 1);
637
561
  drainQueue();
638
562
  };
639
- const timer = setTimeout(() => { markUnusedProbable(id); release(); }, PENDING_TIMEOUT_MS);
563
+ const timer = setTimeout(release, 7000);
640
564
 
641
565
  requestAnimationFrame(() => {
642
566
  try {
643
567
  if (isBlocked()) { clearTimeout(timer); return release(); }
644
568
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
645
- if (!ph?.isConnected) { clearTimeout(timer); markUnusedProbable(id); return release(); }
646
- if (isFilled(ph)) { clearTimeout(timer); clearCooldownIfFilled(id); return release(); }
569
+ if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
647
570
 
648
571
  const t = ts();
649
572
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
@@ -655,6 +578,7 @@
655
578
  const ez = window.ezstandalone;
656
579
  const doShow = () => {
657
580
  try { ez.showAds(id); } catch (_) {}
581
+ scheduleEmptyCheck(id, t);
658
582
  setTimeout(() => { clearTimeout(timer); release(); }, 700);
659
583
  };
660
584
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
@@ -662,6 +586,17 @@
662
586
  });
663
587
  }
664
588
 
589
+ function scheduleEmptyCheck(id, showTs) {
590
+ setTimeout(() => {
591
+ try {
592
+ const ph = document.getElementById(`${PH_PREFIX}${id}`);
593
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
594
+ if (!wrap || !ph?.isConnected) return;
595
+ if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
596
+ wrap.classList.toggle('is-empty', !isFilled(ph));
597
+ } catch (_) {}
598
+ }, EMPTY_CHECK_MS);
599
+ }
665
600
 
666
601
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
667
602
  //
@@ -703,7 +638,6 @@
703
638
  async function runCore() {
704
639
  if (isBlocked()) return 0;
705
640
  patchShowAds();
706
- sweepDeadWraps();
707
641
 
708
642
  const cfg = await fetchConfig();
709
643
  if (!cfg || cfg.excluded) return 0;
@@ -791,9 +725,6 @@
791
725
  S.inflight = 0;
792
726
  S.pending = [];
793
727
  S.pendingSet.clear();
794
- S.pendingShows.clear();
795
- S.cooldownUntil.clear();
796
- for (const id of Array.from(S.fillObsById.keys())) unobserveFill(id);
797
728
  S.burstActive = false;
798
729
  S.runQueued = false;
799
730
  }
@@ -805,12 +736,7 @@
805
736
  const allSel = [SEL.post, SEL.topic, SEL.category];
806
737
  S.domObs = new MutationObserver(muts => {
807
738
  if (S.mutGuard > 0 || isBlocked()) return;
808
- let sawWrapRemoval = false;
809
739
  for (const m of muts) {
810
- for (const n of (m.removedNodes || [])) {
811
- if (n?.nodeType !== 1) continue;
812
- try { if ((n.classList && n.classList.contains(WRAP_CLASS)) || n.querySelector?.(`.${WRAP_CLASS}`)) sawWrapRemoval = true; } catch (_) {}
813
- }
814
740
  for (const n of m.addedNodes) {
815
741
  if (n.nodeType !== 1) continue;
816
742
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
@@ -820,7 +746,6 @@
820
746
  }
821
747
  }
822
748
  }
823
- if (sawWrapRemoval) sweepDeadWraps();
824
749
  });
825
750
  try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
826
751
  }
@@ -922,11 +847,14 @@
922
847
  function bindScroll() {
923
848
  let ticking = false;
924
849
  window.addEventListener('scroll', () => {
850
+ try {
851
+ const y = window.scrollY || window.pageYOffset || 0;
852
+ const dy = y - (S.lastScrollY || 0);
853
+ if (Math.abs(dy) > 2) S.scrollDir = dy > 0 ? 1 : -1;
854
+ S.lastScrollY = y;
855
+ } catch (_) {}
925
856
  if (ticking) return;
926
857
  ticking = true;
927
- const y = window.scrollY || window.pageYOffset || 0;
928
- S.scrollDir = y >= (S.lastScrollY || 0) ? 1 : -1;
929
- S.lastScrollY = y;
930
858
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
931
859
  }, { passive: true });
932
860
  }
@@ -934,6 +862,7 @@
934
862
  // ── Boot ───────────────────────────────────────────────────────────────────
935
863
 
936
864
  S.pageKey = pageKey();
865
+ try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) { S.lastScrollY = 0; }
937
866
  muteConsole();
938
867
  ensureTcfLocator();
939
868
  warmNetwork();