nodebb-plugin-ezoic-infinite 1.7.97 → 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 +198 -144
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.97",
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
@@ -81,6 +81,7 @@
81
81
  const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
82
82
  const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
83
83
  const MAX_INFLIGHT = 4; // max showAds() simultanés
84
+ const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
84
85
  const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
85
86
  const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
86
87
 
@@ -127,6 +128,11 @@
127
128
  pending: [], // ids en attente de slot inflight
128
129
  pendingSet: new Set(),
129
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,
130
136
  runQueued: false,
131
137
  burstActive: false,
132
138
  burstDeadline: 0,
@@ -142,38 +148,34 @@
142
148
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
143
149
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
144
150
 
145
- function ezCmd(fn) {
146
- try {
147
- window.ezstandalone = window.ezstandalone || {};
148
- const ez = window.ezstandalone;
149
- if (Array.isArray(ez.cmd)) ez.cmd.push(fn);
150
- else fn();
151
- } catch (_) {}
152
- }
153
-
154
- function destroyPlaceholderIds(ids) {
155
- try {
156
- const uniq = Array.from(new Set((ids || [])
157
- .map(v => parseInt(v, 10))
158
- .filter(v => Number.isFinite(v) && v > 0)));
159
- if (!uniq.length) return;
160
- ezCmd(() => {
161
- try {
162
- const ez = window.ezstandalone;
163
- if (typeof ez?.destroyPlaceholders === 'function') ez.destroyPlaceholders(uniq);
164
- } catch (_) {}
165
- });
166
- } catch (_) {}
167
- }
168
-
169
- function placeholderCount(id) {
170
- try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length; } catch (_) { return 0; }
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;
171
162
  }
172
163
 
173
164
  function mutate(fn) {
174
165
  S.mutGuard++;
175
166
  try { fn(); } finally { S.mutGuard--; }
176
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
+
177
179
 
178
180
  // ── Config ─────────────────────────────────────────────────────────────────
179
181
 
@@ -335,60 +337,82 @@
335
337
  * displayMore = API Ezoic prévue pour l'infinite scroll.
336
338
  * Priorité : wraps vides d'abord, remplis si nécessaire.
337
339
  */
338
- function recycleAndMove(klass, targetEl, newKey) {
339
- const ez = window.ezstandalone;
340
- if (typeof ez?.destroyPlaceholders !== 'function' ||
341
- typeof ez?.define !== 'function' ||
342
- typeof ez?.displayMore !== 'function') return null;
343
-
344
- const vh = window.innerHeight || 800;
345
- // Seuil : -1vh (hors viewport visible). On appelle unobserve(ph) juste
346
- // après pour neutraliser l'IO — plus de showAds parasite possible.
347
- const threshold = -vh;
348
- let bestEmpty = null, bestEmptyBottom = Infinity;
349
- let bestFilled = null, bestFilledBottom = Infinity;
350
-
351
- document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
352
- try {
353
- const rect = wrap.getBoundingClientRect();
354
- if (rect.bottom > threshold) return;
355
- if (!isFilled(wrap)) {
356
- if (rect.bottom < bestEmptyBottom) { bestEmptyBottom = rect.bottom; bestEmpty = wrap; }
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; }
357
371
  } else {
358
- if (rect.bottom < bestFilledBottom) { bestFilledBottom = rect.bottom; bestFilled = wrap; }
372
+ if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
359
373
  }
360
- } catch (_) {}
361
- });
362
-
363
- const best = bestEmpty ?? bestFilled;
364
- if (!best) return null;
365
- const id = parseInt(best.getAttribute(A_WRAPID), 10);
366
- if (!Number.isFinite(id)) return null;
367
-
368
- const oldKey = best.getAttribute(A_ANCHOR);
369
- // Neutraliser l'IO sur ce wrap avant déplacement évite un showAds
370
- // parasite si le nœud était encore dans la zone IO_MARGIN.
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);
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);
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 (_) {}
383
412
 
384
- // Délais requis : destroyPlaceholders est asynchrone en interne
385
- const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
386
- const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
387
- const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
388
- try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
413
+ return { id, wrap: best };
414
+ }
389
415
 
390
- return { id, wrap: best };
391
- }
392
416
 
393
417
  // ── Wraps DOM — création / suppression ────────────────────────────────────
394
418
 
@@ -424,10 +448,7 @@
424
448
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
425
449
  if (ph instanceof Element) S.io?.unobserve(ph);
426
450
  const id = parseInt(w.getAttribute(A_WRAPID), 10);
427
- if (Number.isFinite(id)) {
428
- destroyPlaceholderIds([id]);
429
- S.mountedIds.delete(id);
430
- }
451
+ if (Number.isFinite(id)) { destroyEzoicId(id); S.mountedIds.delete(id); }
431
452
  const key = w.getAttribute(A_ANCHOR);
432
453
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
433
454
  w.remove();
@@ -537,61 +558,82 @@
537
558
  if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
538
559
  }
539
560
 
540
- function enqueueShow(id) {
541
- if (!id || isBlocked()) return;
542
- if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
543
- if (S.inflight >= MAX_INFLIGHT) {
544
- if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
545
- return;
546
- }
547
- startShow(id);
548
- }
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);
549
599
 
550
- function drainQueue() {
551
- if (isBlocked()) return;
552
- while (S.inflight < MAX_INFLIGHT && S.pending.length) {
553
- const id = S.pending.shift();
554
- S.pendingSet.delete(id);
555
- startShow(id);
556
- }
557
- }
600
+ requestAnimationFrame(() => {
601
+ try {
602
+ if (isBlocked()) { clearTimeout(timer); return release(); }
558
603
 
559
- function startShow(id) {
560
- if (!id || isBlocked()) return;
561
- S.inflight++;
562
- let done = false;
563
- const release = () => {
564
- if (done) return;
565
- done = true;
566
- S.inflight = Math.max(0, S.inflight - 1);
567
- drainQueue();
568
- };
569
- const timer = setTimeout(release, 7000);
604
+ const valid = [];
605
+ const t = ts();
570
606
 
571
- requestAnimationFrame(() => {
572
- try {
573
- 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;
574
610
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
575
- if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
576
- if (placeholderCount(id) !== 1) { 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;
577
614
 
578
- const t = ts();
579
- if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
580
615
  S.lastShow.set(id, t);
581
-
582
616
  try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
617
+ valid.push(id);
618
+ }
583
619
 
584
- window.ezstandalone = window.ezstandalone || {};
585
- const ez = window.ezstandalone;
586
- const doShow = () => {
587
- 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);
588
628
  scheduleEmptyCheck(id, t);
589
- setTimeout(() => { clearTimeout(timer); release(); }, 700);
590
- };
591
- Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
592
- } catch (_) { clearTimeout(timer); release(); }
593
- });
594
- }
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
+
595
637
 
596
638
  function scheduleEmptyCheck(id, showTs) {
597
639
  setTimeout(() => {
@@ -621,15 +663,22 @@
621
663
  const orig = ez.showAds.bind(ez);
622
664
  ez.showAds = function (...args) {
623
665
  if (isBlocked()) return;
624
- 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 = [];
625
668
  const seen = new Set();
626
669
  for (const v of ids) {
627
670
  const id = parseInt(v, 10);
628
671
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
629
672
  if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
630
- if (placeholderCount(id) !== 1) continue;
673
+ if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
631
674
  seen.add(id);
632
- 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
+ }
633
682
  }
634
683
  };
635
684
  } catch (_) {}
@@ -722,18 +771,6 @@
722
771
 
723
772
  function cleanup() {
724
773
  blockedUntil = ts() + 1500;
725
- try {
726
- const ids = Array.from(document.querySelectorAll(`.${WRAP_CLASS}[${A_WRAPID}]`))
727
- .map(w => parseInt(w.getAttribute(A_WRAPID), 10))
728
- .filter(v => Number.isFinite(v) && v > 0);
729
- destroyPlaceholderIds(ids);
730
- ezCmd(() => {
731
- try {
732
- const ez = window.ezstandalone;
733
- if (typeof ez?.destroyAll === 'function') ez.destroyAll();
734
- } catch (_) {}
735
- });
736
- } catch (_) {}
737
774
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
738
775
  S.cfg = null;
739
776
  S.poolsReady = false;
@@ -742,11 +779,15 @@
742
779
  S.mountedIds.clear();
743
780
  S.lastShow.clear();
744
781
  S.wrapByKey.clear();
782
+ S.ezActiveIds.clear();
745
783
  S.inflight = 0;
746
784
  S.pending = [];
747
785
  S.pendingSet.clear();
748
786
  S.burstActive = false;
749
787
  S.runQueued = false;
788
+ S.scrollSpeed = 0;
789
+ S.lastScrollY = 0;
790
+ S.lastScrollTs = 0;
750
791
  }
751
792
 
752
793
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -844,9 +885,7 @@
844
885
  S.pageKey = pageKey();
845
886
  blockedUntil = 0;
846
887
  muteConsole(); ensureTcfLocator(); warmNetwork();
847
- patchShowAds(); getIO(); ensureDomObserver();
848
- ezCmd(() => { try { window.ezstandalone?.showAds?.(); } catch (_) {} });
849
- requestBurst();
888
+ patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
850
889
  });
851
890
 
852
891
  const burstEvts = [
@@ -868,7 +907,22 @@
868
907
 
869
908
  function bindScroll() {
870
909
  let ticking = false;
910
+ try {
911
+ S.lastScrollY = window.scrollY || window.pageYOffset || 0;
912
+ S.lastScrollTs = ts();
913
+ } catch (_) {}
871
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 (_) {}
872
926
  if (ticking) return;
873
927
  ticking = true;
874
928
  requestAnimationFrame(() => { ticking = false; requestBurst(); });