nodebb-plugin-ezoic-infinite 1.7.42 → 1.7.43

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/library.js CHANGED
@@ -15,9 +15,11 @@ function normalizeExcludedGroups(value) {
15
15
  // NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
16
16
  const s = String(value).trim();
17
17
  if (s.startsWith('[')) {
18
- try { const parsed = JSON.parse(s); if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean); } catch (_) {}
18
+ try {
19
+ const parsed = JSON.parse(s);
20
+ if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
21
+ } catch (_) {}
19
22
  }
20
- // Fallback : séparation par virgule
21
23
  return s.split(',').map(v => v.trim()).filter(Boolean);
22
24
  }
23
25
 
@@ -28,7 +30,15 @@ function parseBool(v, def = false) {
28
30
  return s === '1' || s === 'true' || s === 'on' || s === 'yes';
29
31
  }
30
32
 
33
+ // ── Cache groupes (fix #7 : getAllGroups sans cache sur page ACP) ───────────
34
+
35
+ let _groupsCache = null;
36
+ let _groupsCacheAt = 0;
37
+ const GROUPS_TTL = 60_000; // 1 minute
38
+
31
39
  async function getAllGroups() {
40
+ const now = Date.now();
41
+ if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
32
42
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
33
43
  if (!names || !names.length) {
34
44
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
@@ -37,7 +47,9 @@ async function getAllGroups() {
37
47
  const data = await groups.getGroupsData(filtered);
38
48
  const valid = data.filter(g => g && g.name);
39
49
  valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
40
- return valid;
50
+ _groupsCache = valid;
51
+ _groupsCacheAt = now;
52
+ return _groupsCache;
41
53
  }
42
54
 
43
55
  // ── Settings cache (30s TTL) ────────────────────────────────────────────────
@@ -88,7 +100,10 @@ ezstandalone.cmd = ezstandalone.cmd || [];
88
100
  // ── Hooks ──────────────────────────────────────────────────────────────────
89
101
 
90
102
  plugin.onSettingsSet = function (data) {
91
- if (data && data.hash === SETTINGS_KEY) _settingsCache = null;
103
+ if (data && data.hash === SETTINGS_KEY) {
104
+ _settingsCache = null;
105
+ _groupsCache = null; // invalider aussi le cache groupes
106
+ }
92
107
  };
93
108
 
94
109
  plugin.addAdminNavigation = async (header) => {
@@ -101,11 +116,11 @@ plugin.addAdminNavigation = async (header) => {
101
116
  * Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
102
117
  *
103
118
  * NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
104
- * (render.js ligne 232 : templateValues.customHTML = meta.config.customHTML).
105
- * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
106
- * et est rendu via req.app.renderAsync('header', hookReturn.templateData).
107
- * On préfixe customHTML pour que nos scripts passent AVANT le customHTML admin,
108
- * tout en préservant ce dernier.
119
+ * (render.js : templateValues.customHTML = meta.config.customHTML).
120
+ * Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData.
121
+ * On préfixe customHTML pour passer AVANT le customHTML admin tout en le préservant.
122
+ *
123
+ * Fix #3 : erreurs loggées côté serveur plutôt qu'avalées silencieusement.
109
124
  */
110
125
  plugin.injectEzoicHead = async (data) => {
111
126
  try {
@@ -113,17 +128,18 @@ plugin.injectEzoicHead = async (data) => {
113
128
  const uid = data.req?.uid ?? 0;
114
129
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
115
130
  if (!excluded) {
116
- // Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
117
131
  data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
118
132
  }
119
- } catch (_) {}
133
+ } catch (err) {
134
+ // Log l'erreur mais ne pas planter le rendu de la page
135
+ console.error('[ezoic-infinite] injectEzoicHead error:', err.message || err);
136
+ }
120
137
  return data;
121
138
  };
122
139
 
123
140
  plugin.init = async ({ router, middleware }) => {
124
141
  async function render(req, res) {
125
- const settings = await getSettings();
126
- const allGroups = await getAllGroups();
142
+ const [settings, allGroups] = await Promise.all([getSettings(), getAllGroups()]);
127
143
  res.render('admin/plugins/ezoic-infinite', {
128
144
  title: 'Ezoic Infinite Ads',
129
145
  ...settings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.42",
3
+ "version": "1.7.43",
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,69 +1,30 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v36
2
+ * NodeBB Ezoic Infinite Ads — client.js v51
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
- *
8
- * v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
9
- * la position dans le batch courant.
10
- *
7
+ * v19 Intervalle global basé sur l'ordinal absolu (data-index).
11
8
  * v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
12
- * Fix fatal catégories : data-cid au lieu de data-index inexistant.
13
- * IO fixe (une instance, jamais recréée).
14
- * Fix TCF locator : MutationObserver recrée l'iframe si ajaxify la retire.
15
- *
16
- * v25 Fix scroll-up / virtualisation NodeBB : decluster + grace period.
17
- *
18
- * v26 Suppression définitive du recyclage d'id (causait réinjection en haut).
19
- *
20
- * v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB posts).
21
- *
9
+ * IO fixe (une instance, jamais recréée). Fix TCF locator.
10
+ * v25 Fix scroll-up / virtualisation NodeBB.
11
+ * v26 Suppression recyclage d'id (causait réinjection en haut).
12
+ * v27 pruneOrphans supprimé (faux-orphelins sur virtualisation posts).
22
13
  * v28 decluster supprimé. Wraps persistants pendant la session.
23
- *
24
- * v32 Retour anchorAttr = data-index pour ezoic-ad-between.
25
- * data-tid peut être absent clés invalides wraps empilés.
26
- * pruneOrphansBetween réactivé uniquement pour topics de catégorie :
27
- * NodeBB NE virtualise PAS les topics dans une liste de catégorie,
28
- * les ancres (data-index) restent en DOM prune safe et nécessaire
29
- * pour éviter l'empilement après scroll long.
30
- * Toujours désactivé pour les posts : NodeBB virtualise les posts
31
- * hors-viewport faux-orphelins bug réinjection en haut.
32
- *
33
- * v34 moveDistantWrap voir v38.
34
- *
35
- * v50 Suppression de bindLoginCheck() : NodeBB fait un rechargement complet
36
- * après login filter:middleware.renderHeader re-évalue l'exclusion au
37
- * rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
38
- *
39
- * v43 Seuil de recyclage abaissé à -vh + unobserve avant recyclage.
40
- *
41
- * v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
42
- *
43
- * v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
44
- *
45
- * v40 Recyclage slots via destroyPlaceholders+define+displayMore avec délais.
46
- * Séquence : destroy → 300ms → define → 300ms → displayMore.
47
- * Testé manuellement : fonctionne. displayMore = API Ezoic infinite scroll.
48
- *
49
- * v38 Pool épuisé = fin de quota Ezoic par page-view. ez.refresh() interdit
50
- * sur la même page que ez.enable() — supprimé. moveDistantWrap supprimé :
51
- * déplacer un wrap "already defined" ne re-sert aucune pub. Pool épuisé →
52
- * break propre dans injectBetween. muteConsole : ajout warnings refresh.
53
- *
54
- * v36 Optimisations chemin critique (scroll → injectBetween) :
55
- * – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
56
- * sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
57
- * dropWrap et cleanup.
58
- * – wrapIsLive allégé : pour les voisins immédiats on vérifie les
59
- * attributs du nœud lui-même sans querySelector global.
60
- * – MutationObserver : matches() vérifié avant querySelector() pour
61
- * court-circuiter les sous-arbres entiers ajoutés par NodeBB.
62
- *
63
- * v35 Revue complète prod-ready :
64
- * – initPools protégé contre ré-initialisation inutile (S.poolsReady).
65
- * – muteConsole élargit à "No valid placeholders for loadMore".
66
- * – Commentaires et historique nettoyés.
14
+ * v32 pruneOrphansBetween réactivé uniquement pour topics de catégorie.
15
+ * NodeBB NE virtualise PAS les topics → prune safe.
16
+ * Toujours désactivé pour posts (virtualisationfaux-orphelins).
17
+ * v35 initPools protégé (S.poolsReady). muteConsole élargi.
18
+ * v36 S.wrapByKey Map O(1). wrapIsLive allégé. MutationObserver optimisé.
19
+ * v38 ez.refresh() interdit supprimé. Pool épuisé break propre.
20
+ * v40 Recyclage via destroyPlaceholders+define+displayMore (délais 300ms).
21
+ * v43 Seuil recyclage -1vh + unobserve avant déplacement.
22
+ * v49 Fix normalizeExcludedGroups : JSON.parse du tableau NodeBB.
23
+ * v50 bindLoginCheck() supprimé (NodeBB recharge après login).
24
+ * v51 #2 fetchConfig() : backoff 10s sur échec réseau (évite spam API).
25
+ * #4 recycleAndMove : re-observe le placeholder après displayMore.
26
+ * #5 ensureTcfLocator : MutationObserver global disconnecté au cleanup.
27
+ * #6 getIO() recrée l'observer si le type d'écran change (resize).
67
28
  */
68
29
  (function nbbEzoicInfinite() {
69
30
  'use strict';
@@ -127,6 +88,7 @@
127
88
  pending: [], // ids en attente de slot inflight
128
89
  pendingSet: new Set(),
129
90
  wrapByKey: new Map(), // anchorKey → wrap DOM node
91
+ tcfObs: null, // MutationObserver TCF locator
130
92
  runQueued: false,
131
93
  burstActive: false,
132
94
  burstDeadline: 0,
@@ -149,12 +111,17 @@
149
111
 
150
112
  // ── Config ─────────────────────────────────────────────────────────────────
151
113
 
114
+ // Fix #2 : backoff 10s sur échec pour éviter de spammer l'API
115
+ // si le réseau est lent ou la route indisponible.
116
+ let _cfgErrorUntil = 0;
152
117
  async function fetchConfig() {
153
118
  if (S.cfg) return S.cfg;
119
+ if (Date.now() < _cfgErrorUntil) return null;
154
120
  try {
155
121
  const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
156
- if (r.ok) S.cfg = await r.json();
157
- } catch (_) {}
122
+ if (r.ok) { S.cfg = await r.json(); }
123
+ else { _cfgErrorUntil = Date.now() + 10_000; }
124
+ } catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
158
125
  return S.cfg;
159
126
  }
160
127
 
@@ -356,7 +323,15 @@
356
323
  // Délais requis : destroyPlaceholders est asynchrone en interne
357
324
  const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
358
325
  const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
359
- const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
326
+ // Fix #4 : re-observer le ph après displayMore pour déclencher scheduleEmptyCheck
327
+ // si la pub ne charge pas (détection wrap vide).
328
+ const doDisplay = () => {
329
+ try { ez.displayMore([id]); } catch (_) {}
330
+ observePh(id);
331
+ const t = ts();
332
+ try { best.setAttribute(A_SHOWN, String(t)); } catch (_) {}
333
+ scheduleEmptyCheck(id, t);
334
+ };
360
335
  try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
361
336
 
362
337
  return { id, wrap: best };
@@ -486,8 +461,15 @@
486
461
 
487
462
  // ── IntersectionObserver & Show ────────────────────────────────────────────
488
463
 
464
+ // Fix #6 : recréer l'observer si le type d'écran change (rotation, resize).
465
+ // isMobile() est évalué à chaque appel pour détecter un changement.
466
+ let _ioMobile = null; // dernier état mobile/desktop pour lequel l'IO a été créé
489
467
  function getIO() {
490
- if (S.io) return S.io;
468
+ const mobile = isMobile();
469
+ if (S.io && _ioMobile === mobile) return S.io;
470
+ // Type d'écran changé ou première création : (re)créer l'observer
471
+ if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
472
+ _ioMobile = mobile;
491
473
  try {
492
474
  S.io = new IntersectionObserver(entries => {
493
475
  for (const e of entries) {
@@ -496,7 +478,7 @@
496
478
  const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
497
479
  if (Number.isFinite(id) && id > 0) enqueueShow(id);
498
480
  }
499
- }, { root: null, rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
481
+ }, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
500
482
  } catch (_) { S.io = null; }
501
483
  return S.io;
502
484
  }
@@ -702,6 +684,8 @@
702
684
  S.pendingSet.clear();
703
685
  S.burstActive = false;
704
686
  S.runQueued = false;
687
+ if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
688
+ if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
705
689
  }
706
690
 
707
691
  // ── MutationObserver ───────────────────────────────────────────────────────
@@ -758,9 +742,10 @@
758
742
  (document.body || document.documentElement).appendChild(f);
759
743
  };
760
744
  inject();
761
- if (!window.__nbbTcfObs) {
762
- window.__nbbTcfObs = new MutationObserver(inject);
763
- window.__nbbTcfObs.observe(document.documentElement, { childList: true, subtree: true });
745
+ // Fix #5 : ref stockée dans S pour pouvoir déconnecter au cleanup
746
+ if (!S.tcfObs) {
747
+ S.tcfObs = new MutationObserver(inject);
748
+ S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
764
749
  }
765
750
  } catch (_) {}
766
751
  }
@@ -826,6 +811,12 @@
826
811
  ticking = true;
827
812
  requestAnimationFrame(() => { ticking = false; requestBurst(); });
828
813
  }, { passive: true });
814
+ // Fix #6 : détecter rotation/resize pour recréer l'IO avec la bonne marge
815
+ let resizeTimer = 0;
816
+ window.addEventListener('resize', () => {
817
+ clearTimeout(resizeTimer);
818
+ resizeTimer = setTimeout(() => { getIO(); }, 500);
819
+ }, { passive: true });
829
820
  }
830
821
 
831
822
  // ── Boot ───────────────────────────────────────────────────────────────────