nodebb-plugin-ezoic-infinite 1.8.0 → 1.8.2

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.8.0",
3
+ "version": "1.8.2",
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,18 @@
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
+ // Tunables (stables en prod)
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 = 4; // max showAds() simultanés
82
+ const MAX_INSERTS_RUN = 10; // plus réactif si NodeBB injecte en rafale
83
+ const MAX_INFLIGHT = 4; // max showAds() simultanés (garde-fou)
83
84
  const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
84
- const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
85
- const BURST_COOLDOWN_MS = 200; // délai min entre deux déclenchements de burst
85
+ const SHOW_THROTTLE_MS = 600; // anti-spam showAds() par id (plus réactif)
86
+ const SHOW_RELEASE_MS = 700; // relâche inflight après showAds() batché
87
+ const SHOW_FAILSAFE_MS = 7000; // relâche forcée si stack pub lente
88
+ const BATCH_FLUSH_MS = 40; // micro-buffer pour regrouper les ids proches
89
+ const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
90
+ const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
91
+ const BURST_COOLDOWN_MS = 120; // délai min entre deux déclenchements de burst
86
92
 
87
93
  // Marges IO larges et fixes — observer créé une seule fois au boot
88
94
  const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
@@ -126,6 +132,11 @@
126
132
  inflight: 0, // showAds() en cours
127
133
  pending: [], // ids en attente de slot inflight
128
134
  pendingSet: new Set(),
135
+ showBatchTimer: 0,
136
+ destroyBatchTimer: 0,
137
+ destroyPending: [],
138
+ destroyPendingSet: new Set(),
139
+ sweepQueued: false,
129
140
  wrapByKey: new Map(), // anchorKey → wrap DOM node
130
141
  ezActiveIds: new Set(), // ids déjà passés à showAds/displayMore
131
142
  scrollDir: 1, // 1=bas, -1=haut
@@ -147,6 +158,33 @@
147
158
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
148
159
  const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
149
160
 
161
+ function phEl(id) {
162
+ return document.getElementById(`${PH_PREFIX}${id}`);
163
+ }
164
+
165
+ function hasSinglePlaceholder(id) {
166
+ try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
167
+ }
168
+
169
+ function canShowPlaceholderId(id, now = ts()) {
170
+ const n = parseInt(id, 10);
171
+ if (!Number.isFinite(n) || n <= 0) return false;
172
+ if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
173
+ const ph = phEl(n);
174
+ if (!ph?.isConnected || isFilled(ph)) return false;
175
+ if (!hasSinglePlaceholder(n)) return false;
176
+ return true;
177
+ }
178
+
179
+ function queueSweepDeadWraps() {
180
+ if (S.sweepQueued) return;
181
+ S.sweepQueued = true;
182
+ requestAnimationFrame(() => {
183
+ S.sweepQueued = false;
184
+ sweepDeadWraps();
185
+ });
186
+ }
187
+
150
188
  function getDynamicShowBatchMax() {
151
189
  const speed = S.scrollSpeed || 0;
152
190
  const pend = S.pending.length;
@@ -164,15 +202,42 @@
164
202
  S.mutGuard++;
165
203
  try { fn(); } finally { S.mutGuard--; }
166
204
  }
205
+ function scheduleDestroyFlush() {
206
+ if (S.destroyBatchTimer) return;
207
+ S.destroyBatchTimer = setTimeout(() => {
208
+ S.destroyBatchTimer = 0;
209
+ flushDestroyBatch();
210
+ }, DESTROY_FLUSH_MS);
211
+ }
212
+
213
+ function flushDestroyBatch() {
214
+ if (!S.destroyPending.length) return;
215
+ const ids = [];
216
+ while (S.destroyPending.length && ids.length < MAX_DESTROY_BATCH) {
217
+ const id = S.destroyPending.shift();
218
+ S.destroyPendingSet.delete(id);
219
+ if (!Number.isFinite(id) || id <= 0) continue;
220
+ ids.push(id);
221
+ }
222
+ if (ids.length) {
223
+ try {
224
+ const ez = window.ezstandalone;
225
+ const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
226
+ try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
227
+ } catch (_) {}
228
+ }
229
+ if (S.destroyPending.length) scheduleDestroyFlush();
230
+ }
231
+
167
232
  function destroyEzoicId(id) {
168
233
  if (!Number.isFinite(id) || id <= 0) return;
169
234
  if (!S.ezActiveIds.has(id)) return;
170
- try {
171
- const ez = window.ezstandalone;
172
- const run = () => { try { ez?.destroyPlaceholders?.([id]); } catch (_) {} };
173
- try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
174
- } catch (_) {}
175
235
  S.ezActiveIds.delete(id);
236
+ if (!S.destroyPendingSet.has(id)) {
237
+ S.destroyPending.push(id);
238
+ S.destroyPendingSet.add(id);
239
+ }
240
+ scheduleDestroyFlush();
176
241
  }
177
242
 
178
243
 
@@ -329,6 +394,25 @@ function destroyEzoicId(id) {
329
394
  return null;
330
395
  }
331
396
 
397
+ function sweepDeadWraps() {
398
+ // NodeBB peut retirer des nœuds sans passer par dropWrap() (virtualisation / rerender).
399
+ // On libère alors les IDs/keys fantômes pour éviter l'épuisement du pool.
400
+ for (const [key, wrap] of Array.from(S.wrapByKey.entries())) {
401
+ if (wrap?.isConnected) continue;
402
+ S.wrapByKey.delete(key);
403
+ const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
404
+ if (Number.isFinite(id)) {
405
+ S.mountedIds.delete(id);
406
+ S.pendingSet.delete(id);
407
+ S.lastShow.delete(id);
408
+ S.ezActiveIds.delete(id);
409
+ }
410
+ }
411
+ if (S.pending.length) {
412
+ S.pending = S.pending.filter(id => S.pendingSet.has(id));
413
+ }
414
+ }
415
+
332
416
  /**
333
417
  * Pool épuisé : recycle un wrap loin au-dessus du viewport.
334
418
  * Séquence avec délais (destroyPlaceholders est asynchrone) :
@@ -352,13 +436,14 @@ function recycleAndMove(klass, targetEl, newKey) {
352
436
  let bestAnyEmpty = null, bestAnyMetric = Infinity;
353
437
  let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
354
438
 
355
- document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(wrap => {
439
+ for (const wrap of S.wrapByKey.values()) {
440
+ if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
356
441
  try {
357
442
  const rect = wrap.getBoundingClientRect();
358
443
  const isAbove = rect.bottom <= farAbove;
359
444
  const isBelow = rect.top >= farBelow;
360
445
  const anyFar = isAbove || isBelow;
361
- if (!anyFar) return;
446
+ if (!anyFar) continue;
362
447
 
363
448
  const qualifies = preferAbove ? isAbove : isBelow;
364
449
  const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
@@ -377,7 +462,7 @@ function recycleAndMove(klass, targetEl, newKey) {
377
462
  if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
378
463
  }
379
464
  } catch (_) {}
380
- });
465
+ }
381
466
 
382
467
  const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
383
468
  if (!best) return null;
@@ -399,11 +484,8 @@ function recycleAndMove(klass, targetEl, newKey) {
399
484
  S.wrapByKey.set(newKey, best);
400
485
 
401
486
  const doDestroy = () => {
402
- if (S.ezActiveIds.has(id)) {
403
- try { ez.destroyPlaceholders([id]); } catch (_) {}
404
- S.ezActiveIds.delete(id);
405
- }
406
- setTimeout(doDefine, 300);
487
+ if (S.ezActiveIds.has(id)) destroyEzoicId(id);
488
+ setTimeout(doDefine, 330);
407
489
  };
408
490
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
409
491
  const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); } catch (_) {} };
@@ -522,7 +604,8 @@ function recycleAndMove(klass, targetEl, newKey) {
522
604
  const key = anchorKey(klass, el);
523
605
  if (findWrap(key)) continue;
524
606
 
525
- const id = pickId(poolKey);
607
+ let id = pickId(poolKey);
608
+ if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
526
609
  if (id) {
527
610
  const w = insertAfter(el, id, klass, key);
528
611
  if (w) { observePh(id); inserted++; }
@@ -553,15 +636,34 @@ function recycleAndMove(klass, targetEl, newKey) {
553
636
  }
554
637
 
555
638
  function observePh(id) {
556
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
639
+ const ph = phEl(id);
557
640
  if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
641
+ // Fast-path : si déjà proche viewport, ne pas attendre un callback IO complet
642
+ try {
643
+ if (!ph?.isConnected) return;
644
+ const rect = ph.getBoundingClientRect();
645
+ const vh = window.innerHeight || 800;
646
+ const preload = isMobile() ? 1400 : 1000;
647
+ if (rect.top <= vh + preload && rect.bottom >= -preload) enqueueShow(id);
648
+ } catch (_) {}
558
649
  }
559
650
 
560
651
  function enqueueShow(id) {
561
652
  if (!id || isBlocked()) return;
562
- if (ts() - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) return;
563
- if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
564
- drainQueue();
653
+ const n = parseInt(id, 10);
654
+ if (!Number.isFinite(n) || n <= 0) return;
655
+ if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
656
+ if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
657
+ scheduleDrainQueue();
658
+ }
659
+
660
+ function scheduleDrainQueue() {
661
+ if (isBlocked()) return;
662
+ if (S.showBatchTimer) return;
663
+ S.showBatchTimer = setTimeout(() => {
664
+ S.showBatchTimer = 0;
665
+ drainQueue();
666
+ }, BATCH_FLUSH_MS);
565
667
  }
566
668
 
567
669
  function drainQueue() {
@@ -576,10 +678,12 @@ function drainQueue() {
576
678
  const id = S.pending.shift();
577
679
  S.pendingSet.delete(id);
578
680
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
681
+ if (!phEl(id)?.isConnected) continue;
579
682
  seen.add(id);
580
683
  picked.push(id);
581
684
  }
582
685
  if (picked.length) startShowBatch(picked);
686
+ if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
583
687
  }
584
688
 
585
689
  function startShowBatch(ids) {
@@ -594,7 +698,7 @@ function startShowBatch(ids) {
594
698
  S.inflight = Math.max(0, S.inflight - reserve);
595
699
  drainQueue();
596
700
  };
597
- const timer = setTimeout(release, 7000);
701
+ const timer = setTimeout(release, SHOW_FAILSAFE_MS);
598
702
 
599
703
  requestAnimationFrame(() => {
600
704
  try {
@@ -606,10 +710,8 @@ function startShowBatch(ids) {
606
710
  for (const raw of ids) {
607
711
  const id = parseInt(raw, 10);
608
712
  if (!Number.isFinite(id) || id <= 0) continue;
609
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
610
- if (!ph?.isConnected || isFilled(ph)) continue;
611
- if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) continue;
612
- if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
713
+ const ph = phEl(id);
714
+ if (!canShowPlaceholderId(id, t)) continue;
613
715
 
614
716
  S.lastShow.set(id, t);
615
717
  try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
@@ -625,7 +727,7 @@ function startShowBatch(ids) {
625
727
  for (const id of valid) {
626
728
  S.ezActiveIds.add(id);
627
729
  }
628
- setTimeout(() => { clearTimeout(timer); release(); }, 700);
730
+ setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
629
731
  };
630
732
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
631
733
  } catch (_) { clearTimeout(timer); release(); }
@@ -655,8 +757,7 @@ function startShowBatch(ids) {
655
757
  for (const v of ids) {
656
758
  const id = parseInt(v, 10);
657
759
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
658
- if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
659
- if (document.querySelectorAll(`#${PH_PREFIX}${id}`).length !== 1) continue;
760
+ if (!phEl(id)?.isConnected || !hasSinglePlaceholder(id)) continue;
660
761
  seen.add(id);
661
762
  valid.push(id);
662
763
  }
@@ -681,6 +782,7 @@ function startShowBatch(ids) {
681
782
  async function runCore() {
682
783
  if (isBlocked()) return 0;
683
784
  patchShowAds();
785
+ sweepDeadWraps();
684
786
 
685
787
  const cfg = await fetchConfig();
686
788
  if (!cfg || cfg.excluded) return 0;
@@ -747,7 +849,7 @@ function startShowBatch(ids) {
747
849
  S.burstCount++;
748
850
  scheduleRun(n => {
749
851
  if (!n && !S.pending.length) { S.burstActive = false; return; }
750
- setTimeout(step, n > 0 ? 150 : 300);
852
+ setTimeout(step, n > 0 ? 80 : 180);
751
853
  });
752
854
  };
753
855
  step();
@@ -769,8 +871,13 @@ function startShowBatch(ids) {
769
871
  S.inflight = 0;
770
872
  S.pending = [];
771
873
  S.pendingSet.clear();
874
+ if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
875
+ if (S.destroyBatchTimer) { clearTimeout(S.destroyBatchTimer); S.destroyBatchTimer = 0; }
876
+ S.destroyPending = [];
877
+ S.destroyPendingSet.clear();
772
878
  S.burstActive = false;
773
879
  S.runQueued = false;
880
+ S.sweepQueued = false;
774
881
  S.scrollSpeed = 0;
775
882
  S.lastScrollY = 0;
776
883
  S.lastScrollTs = 0;
@@ -784,6 +891,14 @@ function startShowBatch(ids) {
784
891
  S.domObs = new MutationObserver(muts => {
785
892
  if (S.mutGuard > 0 || isBlocked()) return;
786
893
  for (const m of muts) {
894
+ let sawWrapRemoval = false;
895
+ for (const n of m.removedNodes) {
896
+ if (n.nodeType !== 1) continue;
897
+ if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || (n.querySelector && n.querySelector(`.${WRAP_CLASS}`))) {
898
+ sawWrapRemoval = true;
899
+ }
900
+ }
901
+ if (sawWrapRemoval) queueSweepDeadWraps();
787
902
  for (const n of m.addedNodes) {
788
903
  if (n.nodeType !== 1) continue;
789
904
  // matches() d'abord (O(1)), querySelector() seulement si nécessaire
@@ -871,7 +986,7 @@ function startShowBatch(ids) {
871
986
  S.pageKey = pageKey();
872
987
  blockedUntil = 0;
873
988
  muteConsole(); ensureTcfLocator(); warmNetwork();
874
- patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
989
+ patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
875
990
  });
876
991
 
877
992
  const burstEvts = [
package/public/style.css CHANGED
@@ -56,12 +56,6 @@
56
56
  top: auto !important;
57
57
  }
58
58
 
59
- /* ── État vide ────────────────────────────────────────────────────────────── */
60
- /*
61
- Ajouté 20s après showAds si aucun fill détecté.
62
- Collapse à 1px (pas 0) : reste observable par l'IO si le fill arrive tard.
63
- */
64
-
65
59
  /* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
66
60
  .ezoic-ad {
67
61
  margin: 0 !important;