nodebb-plugin-ezoic-infinite 1.7.96 → 1.7.98

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 +206 -150
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.96",
3
+ "version": "1.7.98",
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
@@ -79,15 +79,15 @@
79
79
 
80
80
  const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
81
81
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
82
- const MAX_INSERTS_RUN = 10; // max insertions par appel runCore (plus réactif)
82
+ const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
83
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
84
+ const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
85
+ const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
86
+ const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
86
87
 
87
88
  // Marges IO larges et fixes — observer créé une seule fois au boot
88
89
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
89
90
  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]',
@@ -128,13 +128,16 @@
128
128
  pending: [], // ids en attente de slot inflight
129
129
  pendingSet: new Set(),
130
130
  wrapByKey: new Map(), // anchorKey → wrap DOM node
131
+ ezActiveIds: new Set(), // ids déjà passés à showAds/displayMore
132
+ scrollDir: 1, // 1=bas, -1=haut
133
+ scrollSpeed: 0, // px/s approx (EMA)
134
+ lastScrollY: 0,
135
+ lastScrollTs: 0,
131
136
  runQueued: false,
132
137
  burstActive: false,
133
138
  burstDeadline: 0,
134
139
  burstCount: 0,
135
140
  lastBurstTs: 0,
136
- scrollDir: 1,
137
- lastScrollY: 0,
138
141
  };
139
142
 
140
143
  let blockedUntil = 0;
@@ -145,10 +148,34 @@
145
148
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
146
149
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
147
150
 
151
+ function getDynamicShowBatchMax() {
152
+ const speed = S.scrollSpeed || 0;
153
+ const pend = S.pending.length;
154
+ // Scroll très rapide => petits batches (réduit le churn/unused)
155
+ if (speed > 2600) return 2;
156
+ if (speed > 1400) return 3;
157
+ // Peu de candidats => flush plus vite, inutile d'attendre 4
158
+ if (pend <= 1) return 1;
159
+ if (pend <= 3) return 2;
160
+ // Par défaut compromis dynamique
161
+ return 3;
162
+ }
163
+
148
164
  function mutate(fn) {
149
165
  S.mutGuard++;
150
166
  try { fn(); } finally { S.mutGuard--; }
151
167
  }
168
+ function destroyEzoicId(id) {
169
+ if (!Number.isFinite(id) || id <= 0) return;
170
+ if (!S.ezActiveIds.has(id)) return;
171
+ try {
172
+ const ez = window.ezstandalone;
173
+ const run = () => { try { ez?.destroyPlaceholders?.([id]); } catch (_) {} };
174
+ try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
175
+ } catch (_) {}
176
+ S.ezActiveIds.delete(id);
177
+ }
178
+
152
179
 
153
180
  // ── Config ─────────────────────────────────────────────────────────────────
154
181
 
@@ -310,84 +337,82 @@
310
337
  * displayMore = API Ezoic prévue pour l'infinite scroll.
311
338
  * Priorité : wraps vides d'abord, remplis si nécessaire.
312
339
  */
313
- function recycleAndMove(klass, targetEl, newKey) {
314
- const ez = window.ezstandalone;
315
- if (typeof ez?.destroyPlaceholders !== 'function' ||
316
- typeof ez?.define !== 'function' ||
317
- typeof ez?.displayMore !== 'function') return null;
318
-
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)
323
-
324
- let prefEmpty = null, prefEmptyScore = -Infinity;
325
- let prefFilled = null, prefFilledScore = -Infinity;
326
- let altEmpty = null, altEmptyScore = -Infinity;
327
- let altFilled = null, altFilledScore = -Infinity;
328
-
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;
332
- try {
333
- const rect = wrap.getBoundingClientRect();
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); }
340
+ function recycleAndMove(klass, targetEl, newKey) {
341
+ const ez = window.ezstandalone;
342
+ if (typeof ez?.destroyPlaceholders !== 'function' ||
343
+ typeof ez?.define !== 'function' ||
344
+ typeof ez?.displayMore !== 'function') return null;
345
+
346
+ const vh = window.innerHeight || 800;
347
+ const preferAbove = S.scrollDir >= 0; // scroll bas => recycle en haut
348
+ const farAbove = -vh;
349
+ const farBelow = vh * 2;
350
+
351
+ let bestPrefEmpty = null, bestPrefMetric = Infinity;
352
+ let bestPrefFilled = null, bestPrefFilledMetric = Infinity;
353
+ let bestAnyEmpty = null, bestAnyMetric = Infinity;
354
+ let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
355
+
356
+ document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
357
+ try {
358
+ const rect = wrap.getBoundingClientRect();
359
+ const isAbove = rect.bottom <= farAbove;
360
+ const isBelow = rect.top >= farBelow;
361
+ const anyFar = isAbove || isBelow;
362
+ if (!anyFar) return;
363
+
364
+ const qualifies = preferAbove ? isAbove : isBelow;
365
+ const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
366
+ const filled = isFilled(wrap);
367
+
368
+ if (qualifies) {
369
+ if (!filled) {
370
+ if (metric < bestPrefMetric) { bestPrefMetric = metric; bestPrefEmpty = wrap; }
342
371
  } else {
343
- if (farBelow) { preferred = true; score = Math.abs(rect.top); }
344
- else if (farAbove) { score = Math.abs(rect.bottom); }
372
+ if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
345
373
  }
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
- }
361
- }
362
- } catch (_) {}
374
+ }
375
+ if (!filled) {
376
+ if (metric < bestAnyMetric) { bestAnyMetric = metric; bestAnyEmpty = wrap; }
377
+ } else {
378
+ if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
379
+ }
380
+ } catch (_) {}
381
+ });
382
+
383
+ const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
384
+ if (!best) return null;
385
+ const id = parseInt(best.getAttribute(A_WRAPID), 10);
386
+ if (!Number.isFinite(id)) return null;
387
+
388
+ const oldKey = best.getAttribute(A_ANCHOR);
389
+ try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
390
+ mutate(() => {
391
+ best.setAttribute(A_ANCHOR, newKey);
392
+ best.setAttribute(A_CREATED, String(ts()));
393
+ best.setAttribute(A_SHOWN, '0');
394
+ best.classList.remove('is-empty');
395
+ const ph = best.querySelector(`#${PH_PREFIX}${id}`);
396
+ if (ph) ph.innerHTML = '';
397
+ targetEl.insertAdjacentElement('afterend', best);
398
+ });
399
+ if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
400
+ S.wrapByKey.set(newKey, best);
401
+
402
+ const doDestroy = () => {
403
+ if (S.ezActiveIds.has(id)) {
404
+ try { ez.destroyPlaceholders([id]); } catch (_) {}
405
+ S.ezActiveIds.delete(id);
363
406
  }
407
+ setTimeout(doDefine, 300);
408
+ };
409
+ const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
410
+ const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); } catch (_) {} };
411
+ try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
364
412
 
365
- const best = prefEmpty ?? prefFilled ?? altEmpty ?? altFilled;
366
- if (!best) return null;
367
- const id = parseInt(best.getAttribute(A_WRAPID), 10);
368
- if (!Number.isFinite(id)) return null;
369
-
370
- const oldKey = best.getAttribute(A_ANCHOR);
371
- try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
372
- mutate(() => {
373
- best.setAttribute(A_ANCHOR, newKey);
374
- best.setAttribute(A_CREATED, String(ts()));
375
- best.setAttribute(A_SHOWN, '0');
376
- best.classList.remove('is-empty');
377
- const ph = best.querySelector(`#${PH_PREFIX}${id}`);
378
- if (ph) ph.innerHTML = '';
379
- targetEl.insertAdjacentElement('afterend', best);
380
- });
381
- if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
382
- S.wrapByKey.set(newKey, best);
383
-
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 (_) {} };
387
- try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
413
+ return { id, wrap: best };
414
+ }
388
415
 
389
- return { id, wrap: best };
390
- }
391
416
 
392
417
  // ── Wraps DOM — création / suppression ────────────────────────────────────
393
418
 
@@ -423,7 +448,7 @@
423
448
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
424
449
  if (ph instanceof Element) S.io?.unobserve(ph);
425
450
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
426
- if (Number.isFinite(id)) S.mountedIds.delete(id);
451
+ if (Number.isFinite(id)) { destroyEzoicId(id); S.mountedIds.delete(id); }
427
452
  const key = w.getAttribute(A_ANCHOR);
428
453
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
429
454
  w.remove();
@@ -530,72 +555,85 @@
530
555
 
531
556
  function observePh(id) {
532
557
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
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 (_) {}
543
- }
544
-
545
- function enqueueShow(id) {
546
- if (!id || isBlocked()) return;
547
- if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
548
- if (S.inflight >= MAX_INFLIGHT) {
549
- if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
550
- return;
551
- }
552
- startShow(id);
553
- }
558
+ if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
559
+ }
560
+
561
+ function enqueueShow(id) {
562
+ if (!id || isBlocked()) return;
563
+ if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
564
+ if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
565
+ drainQueue();
566
+ }
567
+
568
+ function drainQueue() {
569
+ if (isBlocked()) return;
570
+ const free = Math.max(0, MAX_INFLIGHT - S.inflight);
571
+ if (!free || !S.pending.length) return;
572
+
573
+ const picked = [];
574
+ const seen = new Set();
575
+ const batchCap = Math.max(1, Math.min(MAX_SHOW_BATCH, free, getDynamicShowBatchMax()));
576
+ while (S.pending.length && picked.length < batchCap) {
577
+ const id = S.pending.shift();
578
+ S.pendingSet.delete(id);
579
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
580
+ seen.add(id);
581
+ picked.push(id);
582
+ }
583
+ if (picked.length) startShowBatch(picked);
584
+ }
585
+
586
+ function startShowBatch(ids) {
587
+ if (!ids?.length || isBlocked()) return;
588
+ const reserve = ids.length;
589
+ S.inflight += reserve;
590
+
591
+ let done = false;
592
+ const release = () => {
593
+ if (done) return;
594
+ done = true;
595
+ S.inflight = Math.max(0, S.inflight - reserve);
596
+ drainQueue();
597
+ };
598
+ const timer = setTimeout(release, 7000);
554
599
 
555
- function drainQueue() {
556
- if (isBlocked()) return;
557
- while (S.inflight < MAX_INFLIGHT && S.pending.length) {
558
- const id = S.pending.shift();
559
- S.pendingSet.delete(id);
560
- startShow(id);
561
- }
562
- }
600
+ requestAnimationFrame(() => {
601
+ try {
602
+ if (isBlocked()) { clearTimeout(timer); return release(); }
563
603
 
564
- function startShow(id) {
565
- if (!id || isBlocked()) return;
566
- S.inflight++;
567
- let done = false;
568
- const release = () => {
569
- if (done) return;
570
- done = true;
571
- S.inflight = Math.max(0, S.inflight - 1);
572
- drainQueue();
573
- };
574
- const timer = setTimeout(release, 7000);
604
+ const valid = [];
605
+ const t = ts();
575
606
 
576
- requestAnimationFrame(() => {
577
- try {
578
- if (isBlocked()) { clearTimeout(timer); return release(); }
607
+ for (const raw of ids) {
608
+ const id = parseInt(raw, 10);
609
+ if (!Number.isFinite(id) || id <= 0) continue;
579
610
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
580
- if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
611
+ if (!ph?.isConnected || isFilled(ph)) continue;
612
+ if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) continue;
613
+ if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
581
614
 
582
- const t = ts();
583
- if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
584
615
  S.lastShow.set(id, t);
585
-
586
616
  try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
617
+ valid.push(id);
618
+ }
587
619
 
588
- window.ezstandalone = window.ezstandalone || {};
589
- const ez = window.ezstandalone;
590
- const doShow = () => {
591
- try { ez.showAds(id); } catch (_) {}
620
+ if (!valid.length) { clearTimeout(timer); return release(); }
621
+
622
+ window.ezstandalone = window.ezstandalone || {};
623
+ const ez = window.ezstandalone;
624
+ const doShow = () => {
625
+ try { ez.showAds(...valid); } catch (_) {}
626
+ for (const id of valid) {
627
+ S.ezActiveIds.add(id);
592
628
  scheduleEmptyCheck(id, t);
593
- setTimeout(() => { clearTimeout(timer); release(); }, 350);
594
- };
595
- Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
596
- } catch (_) { clearTimeout(timer); release(); }
597
- });
598
- }
629
+ }
630
+ setTimeout(() => { clearTimeout(timer); release(); }, 700);
631
+ };
632
+ Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
633
+ } catch (_) { clearTimeout(timer); release(); }
634
+ });
635
+ }
636
+
599
637
 
600
638
  function scheduleEmptyCheck(id, showTs) {
601
639
  setTimeout(() => {
@@ -625,14 +663,22 @@
625
663
  const orig = ez.showAds.bind(ez);
626
664
  ez.showAds = function (...args) {
627
665
  if (isBlocked()) return;
628
- const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
666
+ const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
667
+ const valid = [];
629
668
  const seen = new Set();
630
669
  for (const v of ids) {
631
670
  const id = parseInt(v, 10);
632
671
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
633
672
  if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
673
+ if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
634
674
  seen.add(id);
635
- try { orig(id); } catch (_) {}
675
+ valid.push(id);
676
+ }
677
+ if (!valid.length) return;
678
+ try { orig(...valid); } catch (_) {
679
+ for (const id of valid) {
680
+ try { orig(id); } catch (_) {}
681
+ }
636
682
  }
637
683
  };
638
684
  } catch (_) {}
@@ -715,7 +761,7 @@
715
761
  S.burstCount++;
716
762
  scheduleRun(n => {
717
763
  if (!n && !S.pending.length) { S.burstActive = false; return; }
718
- setTimeout(step, n > 0 ? 80 : 180);
764
+ setTimeout(step, n > 0 ? 150 : 300);
719
765
  });
720
766
  };
721
767
  step();
@@ -733,11 +779,15 @@
733
779
  S.mountedIds.clear();
734
780
  S.lastShow.clear();
735
781
  S.wrapByKey.clear();
782
+ S.ezActiveIds.clear();
736
783
  S.inflight = 0;
737
784
  S.pending = [];
738
785
  S.pendingSet.clear();
739
786
  S.burstActive = false;
740
787
  S.runQueued = false;
788
+ S.scrollSpeed = 0;
789
+ S.lastScrollY = 0;
790
+ S.lastScrollTs = 0;
741
791
  }
742
792
 
743
793
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -857,25 +907,31 @@
857
907
 
858
908
  function bindScroll() {
859
909
  let ticking = false;
910
+ try {
911
+ S.lastScrollY = window.scrollY || window.pageYOffset || 0;
912
+ S.lastScrollTs = ts();
913
+ } catch (_) {}
860
914
  window.addEventListener('scroll', () => {
915
+ try {
916
+ const y = window.scrollY || window.pageYOffset || 0;
917
+ const t = ts();
918
+ const dy = y - (S.lastScrollY || 0);
919
+ const dt = Math.max(1, t - (S.lastScrollTs || t));
920
+ if (Math.abs(dy) > 1) S.scrollDir = dy >= 0 ? 1 : -1;
921
+ const inst = Math.abs(dy) * 1000 / dt;
922
+ S.scrollSpeed = S.scrollSpeed ? (S.scrollSpeed * 0.7 + inst * 0.3) : inst;
923
+ S.lastScrollY = y;
924
+ S.lastScrollTs = t;
925
+ } catch (_) {}
861
926
  if (ticking) return;
862
927
  ticking = true;
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
- });
928
+ requestAnimationFrame(() => { ticking = false; requestBurst(); });
872
929
  }, { passive: true });
873
930
  }
874
931
 
875
932
  // ── Boot ───────────────────────────────────────────────────────────────────
876
933
 
877
934
  S.pageKey = pageKey();
878
- try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) {}
879
935
  muteConsole();
880
936
  ensureTcfLocator();
881
937
  warmNetwork();