nodebb-plugin-ezoic-infinite 1.7.94 → 1.7.96

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 +92 -150
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.94",
3
+ "version": "1.7.96",
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,17 +77,17 @@
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
- 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
86
- const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
82
+ const MAX_INSERTS_RUN = 10; // max insertions par appel runCore (plus réactif)
83
+ const MAX_INFLIGHT = 4; // max showAds() simultanés
84
+ const SHOW_THROTTLE_MS = 600; // anti-spam showAds() par id (plus réactif)
85
+ const BURST_COOLDOWN_MS = 120; // 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
89
88
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
90
89
  const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
90
+ const FAST_SHOW_MARGIN_PX = 900; // show immédiat si slot déjà proche viewport
91
91
 
92
92
  const SEL = {
93
93
  post: '[component="post"][data-pid]',
@@ -125,19 +125,16 @@
125
125
  domObs: null,
126
126
  mutGuard: 0, // >0 : mutations DOM en cours (MutationObserver ignoré)
127
127
  inflight: 0, // showAds() en cours
128
- pending: [], // ids en attente de slot inflight (triés par priorité)
128
+ pending: [], // ids en attente de slot inflight
129
129
  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
130
  wrapByKey: new Map(), // anchorKey → wrap DOM node
134
131
  runQueued: false,
135
132
  burstActive: false,
136
133
  burstDeadline: 0,
137
134
  burstCount: 0,
138
135
  lastBurstTs: 0,
139
- lastScrollY: 0,
140
136
  scrollDir: 1,
137
+ lastScrollY: 0,
141
138
  };
142
139
 
143
140
  let blockedUntil = 0;
@@ -147,69 +144,6 @@
147
144
  const isMobile = () => { try { return window.innerWidth < 768; } catch (_) { return false; } };
148
145
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
149
146
  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
147
 
214
148
  function mutate(fn) {
215
149
  S.mutGuard++;
@@ -351,21 +285,6 @@
351
285
  return (w?.isConnected) ? w : null;
352
286
  }
353
287
 
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
288
  // ── Pool ───────────────────────────────────────────────────────────────────
370
289
 
371
290
  /**
@@ -397,40 +316,59 @@
397
316
  typeof ez?.define !== 'function' ||
398
317
  typeof ez?.displayMore !== 'function') return null;
399
318
 
400
- const vh = window.innerHeight || 800;
401
- const targetRect = targetEl?.getBoundingClientRect?.();
402
- const dir = S.scrollDir >= 0 ? 1 : -1;
319
+ const vh = window.innerHeight || 800;
320
+ const dir = S.scrollDir >= 0 ? 1 : -1;
321
+ const farTopThreshold = -vh; // hors écran au-dessus
322
+ const farBottomThreshold = vh * 2; // loin sous le viewport (scroll-up)
403
323
 
404
- let best = null, bestScore = Infinity;
405
- let fallback = null, fallbackScore = Infinity;
324
+ let prefEmpty = null, prefEmptyScore = -Infinity;
325
+ let prefFilled = null, prefFilledScore = -Infinity;
326
+ let altEmpty = null, altEmptyScore = -Infinity;
327
+ let altFilled = null, altFilledScore = -Infinity;
406
328
 
407
329
  for (const wrap of S.wrapByKey.values()) {
330
+ if (!wrap?.isConnected) continue;
331
+ if (!wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
408
332
  try {
409
- if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
410
333
  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;
334
+ const farAbove = rect.bottom <= farTopThreshold;
335
+ const farBelow = rect.top >= farBottomThreshold;
336
+ let preferred = false;
337
+ let score = -Infinity;
338
+
339
+ if (dir >= 0) {
340
+ if (farAbove) { preferred = true; score = Math.abs(rect.bottom); }
341
+ else if (farBelow) { score = Math.abs(rect.top); }
342
+ } else {
343
+ if (farBelow) { preferred = true; score = Math.abs(rect.top); }
344
+ else if (farAbove) { score = Math.abs(rect.bottom); }
345
+ }
346
+ if (score === -Infinity) continue;
347
+
348
+ const filled = isFilled(wrap);
349
+ if (preferred) {
350
+ if (!filled) {
351
+ if (score > prefEmptyScore) { prefEmptyScore = score; prefEmpty = wrap; }
352
+ } else {
353
+ if (score > prefFilledScore) { prefFilledScore = score; prefFilled = wrap; }
354
+ }
355
+ } else {
356
+ if (!filled) {
357
+ if (score > altEmptyScore) { altEmptyScore = score; altEmpty = wrap; }
358
+ } else {
359
+ if (score > altFilledScore) { altFilledScore = score; altFilled = wrap; }
360
+ }
423
361
  }
424
362
  } catch (_) {}
425
363
  }
426
364
 
427
- best = best || fallback;
365
+ const best = prefEmpty ?? prefFilled ?? altEmpty ?? altFilled;
428
366
  if (!best) return null;
429
367
  const id = parseInt(best.getAttribute(A_WRAPID), 10);
430
368
  if (!Number.isFinite(id)) return null;
431
369
 
432
370
  const oldKey = best.getAttribute(A_ANCHOR);
433
- try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) { S.io?.unobserve(ph); unobserveFill(id); } } catch (_) {}
371
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
434
372
  mutate(() => {
435
373
  best.setAttribute(A_ANCHOR, newKey);
436
374
  best.setAttribute(A_CREATED, String(ts()));
@@ -443,9 +381,9 @@
443
381
  if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
444
382
  S.wrapByKey.set(newKey, best);
445
383
 
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 (_) {} };
384
+ const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 220); };
385
+ const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay,220); };
386
+ const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
449
387
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
450
388
 
451
389
  return { id, wrap: best };
@@ -485,7 +423,6 @@
485
423
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
486
424
  if (ph instanceof Element) S.io?.unobserve(ph);
487
425
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
488
- if (Number.isFinite(id)) unobserveFill(id);
489
426
  if (Number.isFinite(id)) S.mountedIds.delete(id);
490
427
  const key = w.getAttribute(A_ANCHOR);
491
428
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
@@ -549,7 +486,6 @@
549
486
  function injectBetween(klass, items, interval, showFirst, poolKey) {
550
487
  if (!items.length) return 0;
551
488
  let inserted = 0;
552
- sweepDeadWraps();
553
489
 
554
490
  for (const el of items) {
555
491
  if (inserted >= MAX_INSERTS_RUN) break;
@@ -562,15 +498,13 @@
562
498
  const key = anchorKey(klass, el);
563
499
  if (findWrap(key)) continue;
564
500
 
565
- let id = pickId(poolKey);
566
- if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
501
+ const id = pickId(poolKey);
567
502
  if (id) {
568
503
  const w = insertAfter(el, id, klass, key);
569
504
  if (w) { observePh(id); inserted++; }
570
505
  } else {
571
506
  const recycled = recycleAndMove(klass, el, key);
572
507
  if (!recycled) break;
573
- observePh(recycled.id);
574
508
  inserted++;
575
509
  }
576
510
  }
@@ -596,54 +530,54 @@
596
530
 
597
531
  function observePh(id) {
598
532
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
599
- if (ph?.isConnected) {
600
- try { getIO()?.observe(ph); } catch (_) {}
601
- watchPlaceholderFill(id);
602
- }
533
+ if (!ph?.isConnected) return;
534
+ try { getIO()?.observe(ph); } catch (_) {}
535
+ try {
536
+ const r = ph.getBoundingClientRect();
537
+ const vh = window.innerHeight || 800;
538
+ if (r.bottom >= -FAST_SHOW_MARGIN_PX && r.top <= vh + FAST_SHOW_MARGIN_PX) {
539
+ const pid = parseInt(ph.getAttribute('data-ezoic-id'), 10);
540
+ if (Number.isFinite(pid) && pid > 0) enqueueShow(pid);
541
+ }
542
+ } catch (_) {}
603
543
  }
604
544
 
605
545
  function enqueueShow(id) {
606
546
  if (!id || isBlocked()) return;
607
- if (isCooling(id) || S.pendingShows.has(id)) return;
608
547
  if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
609
- if (S.inflight >= MAX_INFLIGHT) { pendingEnqueue(id); return; }
548
+ if (S.inflight >= MAX_INFLIGHT) {
549
+ if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
550
+ return;
551
+ }
610
552
  startShow(id);
611
553
  }
612
554
 
613
555
  function drainQueue() {
614
556
  if (isBlocked()) return;
615
557
  while (S.inflight < MAX_INFLIGHT && S.pending.length) {
616
- const item = S.pending.shift();
617
- const id = item?.id ?? item;
558
+ const id = S.pending.shift();
618
559
  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
560
  startShow(id);
623
561
  }
624
562
  }
625
563
 
626
564
  function startShow(id) {
627
565
  if (!id || isBlocked()) return;
628
- if (S.pendingShows.has(id) || isCooling(id)) return;
629
- S.pendingShows.add(id);
630
566
  S.inflight++;
631
567
  let done = false;
632
568
  const release = () => {
633
569
  if (done) return;
634
570
  done = true;
635
- S.pendingShows.delete(id);
636
571
  S.inflight = Math.max(0, S.inflight - 1);
637
572
  drainQueue();
638
573
  };
639
- const timer = setTimeout(() => { markUnusedProbable(id); release(); }, PENDING_TIMEOUT_MS);
574
+ const timer = setTimeout(release, 7000);
640
575
 
641
576
  requestAnimationFrame(() => {
642
577
  try {
643
578
  if (isBlocked()) { clearTimeout(timer); return release(); }
644
579
  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(); }
580
+ if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
647
581
 
648
582
  const t = ts();
649
583
  if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
@@ -655,13 +589,25 @@
655
589
  const ez = window.ezstandalone;
656
590
  const doShow = () => {
657
591
  try { ez.showAds(id); } catch (_) {}
658
- setTimeout(() => { clearTimeout(timer); release(); }, 700);
592
+ scheduleEmptyCheck(id, t);
593
+ setTimeout(() => { clearTimeout(timer); release(); }, 350);
659
594
  };
660
595
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
661
596
  } catch (_) { clearTimeout(timer); release(); }
662
597
  });
663
598
  }
664
599
 
600
+ function scheduleEmptyCheck(id, showTs) {
601
+ setTimeout(() => {
602
+ try {
603
+ const ph = document.getElementById(`${PH_PREFIX}${id}`);
604
+ const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
605
+ if (!wrap || !ph?.isConnected) return;
606
+ if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
607
+ wrap.classList.toggle('is-empty', !isFilled(ph));
608
+ } catch (_) {}
609
+ }, EMPTY_CHECK_MS);
610
+ }
665
611
 
666
612
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
667
613
  //
@@ -703,7 +649,6 @@
703
649
  async function runCore() {
704
650
  if (isBlocked()) return 0;
705
651
  patchShowAds();
706
- sweepDeadWraps();
707
652
 
708
653
  const cfg = await fetchConfig();
709
654
  if (!cfg || cfg.excluded) return 0;
@@ -770,7 +715,7 @@
770
715
  S.burstCount++;
771
716
  scheduleRun(n => {
772
717
  if (!n && !S.pending.length) { S.burstActive = false; return; }
773
- setTimeout(step, n > 0 ? 150 : 300);
718
+ setTimeout(step, n > 0 ? 80 : 180);
774
719
  });
775
720
  };
776
721
  step();
@@ -791,9 +736,6 @@
791
736
  S.inflight = 0;
792
737
  S.pending = [];
793
738
  S.pendingSet.clear();
794
- S.pendingShows.clear();
795
- S.cooldownUntil.clear();
796
- for (const id of Array.from(S.fillObsById.keys())) unobserveFill(id);
797
739
  S.burstActive = false;
798
740
  S.runQueued = false;
799
741
  }
@@ -805,12 +747,7 @@
805
747
  const allSel = [SEL.post, SEL.topic, SEL.category];
806
748
  S.domObs = new MutationObserver(muts => {
807
749
  if (S.mutGuard > 0 || isBlocked()) return;
808
- let sawWrapRemoval = false;
809
750
  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
751
  for (const n of m.addedNodes) {
815
752
  if (n.nodeType !== 1) continue;
816
753
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
@@ -820,7 +757,6 @@
820
757
  }
821
758
  }
822
759
  }
823
- if (sawWrapRemoval) sweepDeadWraps();
824
760
  });
825
761
  try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
826
762
  }
@@ -924,16 +860,22 @@
924
860
  window.addEventListener('scroll', () => {
925
861
  if (ticking) return;
926
862
  ticking = true;
927
- const y = window.scrollY || window.pageYOffset || 0;
928
- S.scrollDir = y >= (S.lastScrollY || 0) ? 1 : -1;
929
- S.lastScrollY = y;
930
- requestAnimationFrame(() => { ticking = false; requestBurst(); });
863
+ requestAnimationFrame(() => {
864
+ ticking = false;
865
+ try {
866
+ const y = window.scrollY || window.pageYOffset || 0;
867
+ S.scrollDir = (y < (S.lastScrollY || 0)) ? -1 : 1;
868
+ S.lastScrollY = y;
869
+ } catch (_) {}
870
+ requestBurst();
871
+ });
931
872
  }, { passive: true });
932
873
  }
933
874
 
934
875
  // ── Boot ───────────────────────────────────────────────────────────────────
935
876
 
936
877
  S.pageKey = pageKey();
878
+ try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) {}
937
879
  muteConsole();
938
880
  ensureTcfLocator();
939
881
  warmNetwork();