nodebb-plugin-ezoic-infinite 1.7.19 → 1.7.21

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 +33 -149
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.19",
3
+ "version": "1.7.21",
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
@@ -1,10 +1,10 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v25
2
+ * NodeBB Ezoic Infinite Ads — client.js v28
3
3
  *
4
4
  * Historique des corrections majeures
5
5
  * ────────────────────────────────────
6
6
  * v18 Ancrage stable par data-pid/data-index au lieu d'ordinalMap fragile.
7
- * Suppression du recyclage de wraps (moveWrapAfter). Cleanup complet navigation.
7
+ * Suppression du recyclage de wraps. Cleanup complet navigation.
8
8
  *
9
9
  * v19 Intervalle global basé sur l'ordinal absolu (data-index) et non sur
10
10
  * la position dans le batch courant.
@@ -16,17 +16,23 @@
16
16
  * Fix unobserve(null) → corruption IO → pubads error au scroll retour.
17
17
  * Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
18
18
  *
19
- * v25 Base v20.1 avec :
20
- * Fix scroll-up / virtualisation NodeBB :
21
- * – pruneOrphans : PRUNE_STABLE_MS = 45 s, isFilled guard en premier.
19
+ * v25 Table KIND unifiée avec baseTag + ordinalAttr.
20
+ * Fix scroll-up / virtualisation NodeBB :
22
21
  * – decluster : isFilled en premier, A_CREATED grace period (FILL_GRACE_MS).
23
- * Recyclage d'id (pool épuisé en infinite scroll) :
24
- * pickRecyclableWrap() : sélectionne le wrap vide le plus loin au-dessus
25
- * du viewport (seuil -6 × vh), jamais pour ezoic-ad-message.
26
- * moveWrapAfter() : déplace le wrap vers sa nouvelle ancre.
27
- * scrollDir tracking pour n'autoriser le recyclage qu'en scroll down.
28
- * • Table KIND unifiée avec baseTag + ordinalAttr + recyclable flag.
29
- * ordinal() : utilise KIND[klass].ordinalAttr, fallback positionnel propre.
22
+ * Tentative recyclage d'id (v25) cause exactement le même bug (wraps
23
+ * déplacés laissent les positions originales libres réinjection en haut).
24
+ *
25
+ * v26 Suppression définitive du recyclage d'id.
26
+ * KIND simplifié.
27
+ *
28
+ * v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB).
29
+ *
30
+ * v28 decluster supprimé.
31
+ * Analyse : decluster appelait dropWrap() qui faisait S.mountedIds.delete(id).
32
+ * Un wrap vide adjacent à un autre wrap → supprimé → id libéré → réinjecté
33
+ * en haut au prochain scroll. Exactement le bug observé.
34
+ * Les deux mécanismes de nettoyage actif (pruneOrphans + decluster) sont
35
+ * maintenant retirés. Seul cleanup() à la navigation peut supprimer des wraps.
30
36
  */
31
37
  (function () {
32
38
  'use strict';
@@ -40,10 +46,7 @@
40
46
  const A_CREATED = 'data-ezoic-created'; // timestamp création ms
41
47
  const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
42
48
 
43
- const PRUNE_STABLE_MS = 45_000; // délai avant pruning (évite faux-orphelins scroll-up)
44
- const FILL_GRACE_MS = 25_000; // fenêtre fill async Ezoic (SSP auction)
45
49
  const EMPTY_CHECK_MS = 20_000; // délai collapse wrap vide post-show
46
- const RECYCLE_THRESHOLD = 6; // nb de viewports au-dessus du seuil de recyclage
47
50
  const MAX_INSERTS_RUN = 6;
48
51
  const MAX_INFLIGHT = 4;
49
52
  const SHOW_THROTTLE_MS = 900;
@@ -69,13 +72,11 @@
69
72
  * data-pid posts / data-index topics / data-cid catégories
70
73
  * ordinalAttr: attribut 0-based pour calcul de l'intervalle
71
74
  * null → fallback positionnel (catégories)
72
- * recyclable : autoriser le recyclage d'id quand le pool est épuisé
73
- * false pour ezoic-ad-message (sauts visuels indésirables)
74
75
  */
75
76
  const KIND = {
76
- 'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index', recyclable: false },
77
- 'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index', recyclable: true },
78
- 'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null, recyclable: true },
77
+ 'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
78
+ 'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
79
+ 'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
79
80
  };
80
81
 
81
82
  // ── État ───────────────────────────────────────────────────────────────────
@@ -98,8 +99,6 @@
98
99
  burstDeadline: 0,
99
100
  burstCount: 0,
100
101
  lastBurstTs: 0,
101
- scrollDir: 1, // 1 = down, -1 = up
102
- lastScrollY: 0,
103
102
  };
104
103
 
105
104
  let blockedUntil = 0;
@@ -223,49 +222,6 @@
223
222
  return null;
224
223
  }
225
224
 
226
- // ── Recyclage d'id ─────────────────────────────────────────────────────────
227
-
228
- /**
229
- * Sélectionne le wrap vide le plus éloigné au-dessus du viewport.
230
- * Conditions : kindClass.recyclable = true, scroll vers le bas,
231
- * wrap vide (non filled), rect.bottom < -(RECYCLE_THRESHOLD × vh).
232
- */
233
- function pickRecyclableWrap(klass) {
234
- if (!KIND[klass]?.recyclable) return null;
235
- if (S.scrollDir < 0) return null;
236
-
237
- const vh = Math.max(300, window.innerHeight || 800);
238
- const threshold = -(vh * RECYCLE_THRESHOLD);
239
- let best = null, bestBottom = Infinity;
240
-
241
- for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
242
- if (!w.isConnected || isFilled(w)) continue;
243
- try {
244
- const rect = w.getBoundingClientRect();
245
- if (rect.bottom < threshold && rect.bottom < bestBottom) {
246
- bestBottom = rect.bottom;
247
- best = w;
248
- }
249
- } catch (_) {}
250
- }
251
- return best;
252
- }
253
-
254
- /**
255
- * Déplace un wrap recyclé vers sa nouvelle ancre el.
256
- * Réinitialise A_ANCHOR, A_CREATED, supprime A_SHOWN.
257
- */
258
- function moveWrapAfter(el, wrap, newKey) {
259
- try {
260
- if (!el || !wrap?.isConnected) return null;
261
- wrap.setAttribute(A_ANCHOR, newKey);
262
- wrap.setAttribute(A_CREATED, String(ts()));
263
- wrap.removeAttribute(A_SHOWN);
264
- mutate(() => el.insertAdjacentElement('afterend', wrap));
265
- return wrap;
266
- } catch (_) { return null; }
267
- }
268
-
269
225
  // ── Wraps DOM ──────────────────────────────────────────────────────────────
270
226
 
271
227
  function makeWrap(id, klass, key) {
@@ -303,66 +259,16 @@
303
259
  } catch (_) {}
304
260
  }
305
261
 
306
- // ── Prune ──────────────────────────────────────────────────────────────────
262
+ // ── Prune : désactivé ─────────────────────────────────────────────────────
263
+ //
264
+ // pruneOrphans() a été supprimé car il causait le bug "pubs en haut".
265
+ // NodeBB virtualise les posts hors viewport → les ancres disparaissent du DOM
266
+ // temporairement → pruneOrphans supprimait les wraps → scroll retour → les
267
+ // ancres revenaient → injectBetween réinjectait tout en haut.
268
+ //
269
+ // Les wraps ne sont supprimés que par cleanup() à chaque navigation ajaxify.
270
+ // decluster() et pruneOrphans() sont désactivés — voir v28.
307
271
 
308
- /**
309
- * Supprime les wraps VIDES dont l'ancre a disparu du DOM.
310
- *
311
- * isFilled en premier : un wrap rempli n'est JAMAIS supprimé.
312
- * PRUNE_STABLE_MS (45 s) : pendant cette fenêtre une ancre absente est
313
- * considérée comme virtualisée par NodeBB (scroll up), pas comme orphelin réel.
314
- */
315
- function pruneOrphans(klass) {
316
- const meta = KIND[klass];
317
- if (!meta) return;
318
-
319
- for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
320
- if (isFilled(w)) continue;
321
- if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < PRUNE_STABLE_MS) continue;
322
-
323
- const key = w.getAttribute(A_ANCHOR) ?? '';
324
- const sid = key.slice(klass.length + 1);
325
- if (!sid) { mutate(() => dropWrap(w)); continue; }
326
-
327
- const sel = `${meta.baseTag}[${meta.anchorAttr}="${sid.replace(/"/g, '\\"')}"]`;
328
- const anchorEl = document.querySelector(sel);
329
- if (!anchorEl || !anchorEl.isConnected) mutate(() => dropWrap(w));
330
- }
331
- }
332
-
333
- // ── Decluster ──────────────────────────────────────────────────────────────
334
-
335
- /**
336
- * Deux wraps adjacents → supprimer le courant s'il est vide et hors grâce.
337
- * Guards dans l'ordre :
338
- * 1. isFilled(w) → jamais toucher un wrap rempli
339
- * 2. A_CREATED < FILL_GRACE → wrap trop récent (pas encore showAds'd)
340
- * 3. A_SHOWN grace → fill en cours
341
- * 4. isFilled(prev) → voisin rempli, intouchable → break
342
- * 5. A_CREATED prev grace → voisin trop récent → break
343
- * 6. A_SHOWN prev grace → break
344
- * → les deux vides et hors grâce : supprimer le courant
345
- */
346
- function decluster(klass) {
347
- for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
348
- if (isFilled(w)) continue;
349
- if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) continue;
350
- const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
351
- if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
352
-
353
- let prev = w.previousElementSibling, steps = 0;
354
- while (prev && steps++ < 3) {
355
- if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
356
- if (isFilled(prev)) break;
357
- if (ts() - parseInt(prev.getAttribute(A_CREATED) || '0', 10) < FILL_GRACE_MS) break;
358
- const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
359
- if (pShown && ts() - pShown < FILL_GRACE_MS) break;
360
-
361
- mutate(() => dropWrap(w));
362
- break;
363
- }
364
- }
365
- }
366
272
 
367
273
  // ── Injection ──────────────────────────────────────────────────────────────
368
274
 
@@ -401,23 +307,11 @@
401
307
  const key = anchorKey(klass, el);
402
308
  if (findWrap(key)) continue;
403
309
 
404
- // 1. Tentative pool normal
405
310
  const id = pickId(poolKey);
406
- if (id) {
407
- const w = insertAfter(el, id, klass, key);
408
- if (w) { observePh(id); inserted++; }
409
- continue;
410
- }
311
+ if (!id) continue; // pool épuisé : tous les ids sont montés, on passe au suivant
411
312
 
412
- // 2. Pool épuisé tentative de recyclage
413
- const recyclable = pickRecyclableWrap(klass);
414
- if (recyclable) {
415
- const rid = parseInt(recyclable.getAttribute(A_WRAPID), 10);
416
- const w = moveWrapAfter(el, recyclable, key);
417
- if (w && Number.isFinite(rid)) { observePh(rid); inserted++; }
418
- }
419
- // Pool épuisé et pas de recyclage : on continue (items suivants peuvent
420
- // avoir un wrap existant via findWrap, on ne break pas)
313
+ const w = insertAfter(el, id, klass, key);
314
+ if (w) { observePh(id); inserted++; }
421
315
  }
422
316
  return inserted;
423
317
  }
@@ -558,9 +452,7 @@
558
452
  const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
559
453
  if (!normBool(cfgEnable)) return 0;
560
454
  const interval = Math.max(1, parseInt(cfgInterval, 10) || 3);
561
- pruneOrphans(klass);
562
455
  const n = injectBetween(klass, getItems(), interval, normBool(cfgShowFirst), poolKey);
563
- if (n) decluster(klass);
564
456
  return n;
565
457
  };
566
458
 
@@ -743,13 +635,6 @@
743
635
  function bindScroll() {
744
636
  let ticking = false;
745
637
  window.addEventListener('scroll', () => {
746
- // Suivi direction du scroll (nécessaire pour le recyclage conditionnel)
747
- try {
748
- const y = window.scrollY || window.pageYOffset || 0;
749
- const d = y - S.lastScrollY;
750
- if (Math.abs(d) > 4) { S.scrollDir = d > 0 ? 1 : -1; S.lastScrollY = y; }
751
- } catch (_) {}
752
-
753
638
  if (ticking) return;
754
639
  ticking = true;
755
640
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
@@ -759,7 +644,6 @@
759
644
  // ── Boot ───────────────────────────────────────────────────────────────────
760
645
 
761
646
  S.pageKey = pageKey();
762
- try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) {}
763
647
  muteConsole();
764
648
  ensureTcfLocator();
765
649
  warmNetwork();