nodebb-plugin-ezoic-infinite 1.4.8 → 1.4.10

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
@@ -27,13 +27,21 @@ async function getAllGroups() {
27
27
  }
28
28
  const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
29
  const data = await groups.getGroupsData(filtered);
30
- // Sort alphabetically for ACP usability
31
- data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
32
- return data;
30
+ // Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
31
+ const valid = data.filter(g => g && g.name);
32
+ valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
33
+ return valid;
33
34
  }
35
+ let _settingsCache = null;
36
+ let _settingsCacheAt = 0;
37
+ const SETTINGS_TTL = 30000; // 30s
38
+
34
39
  async function getSettings() {
40
+ const now = Date.now();
41
+ if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
35
42
  const s = await meta.settings.get(SETTINGS_KEY);
36
- return {
43
+ _settingsCacheAt = Date.now();
44
+ _settingsCache = {
37
45
  // Between-post ads (simple blocks) in category topic list
38
46
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
39
47
  showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
@@ -54,6 +62,7 @@ async function getSettings() {
54
62
 
55
63
  excludedGroups: normalizeExcludedGroups(s.excludedGroups),
56
64
  };
65
+ return _settingsCache;
57
66
  }
58
67
 
59
68
  async function isUserExcluded(uid, excludedGroups) {
@@ -62,6 +71,13 @@ async function isUserExcluded(uid, excludedGroups) {
62
71
  return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
63
72
  }
64
73
 
74
+ plugin.onSettingsSet = function (data) {
75
+ // Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
76
+ if (data && data.hash === SETTINGS_KEY) {
77
+ _settingsCache = null;
78
+ }
79
+ };
80
+
65
81
  plugin.addAdminNavigation = async (header) => {
66
82
  header.plugins = header.plugins || [];
67
83
  header.plugins.push({
@@ -89,7 +105,7 @@ plugin.init = async ({ router, middleware }) => {
89
105
  router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
90
106
  router.get('/api/admin/plugins/ezoic-infinite', render);
91
107
 
92
- router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
108
+ router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
93
109
  const settings = await getSettings();
94
110
  const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
95
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -11,6 +11,10 @@
11
11
  {
12
12
  "hook": "filter:admin.header.build",
13
13
  "method": "addAdminNavigation"
14
+ },
15
+ {
16
+ "hook": "action:settings.set",
17
+ "method": "onSettingsSet"
14
18
  }
15
19
  ],
16
20
  "staticDirs": {
package/public/admin.js CHANGED
@@ -13,11 +13,10 @@
13
13
  e.preventDefault();
14
14
 
15
15
  Settings.save('ezoic-infinite', $form, function () {
16
- // Toast vert (NodeBB core)
17
16
  if (alerts && typeof alerts.success === 'function') {
18
- alerts.success('Enregistré');
17
+ alerts.success('[[admin/settings:saved]]');
19
18
  } else if (window.app && typeof window.app.alertSuccess === 'function') {
20
- window.app.alertSuccess('Enregistré');
19
+ window.app.alertSuccess('[[admin/settings:saved]]');
21
20
  }
22
21
  });
23
22
  });
package/public/client.js CHANGED
@@ -31,7 +31,6 @@
31
31
  liveBetween: [],
32
32
  liveMessage: [],
33
33
  liveCategory: [],
34
- usedCategories: new Set(),
35
34
 
36
35
  lastShowById: new Map(),
37
36
  pendingById: new Set(),
@@ -48,6 +47,8 @@
48
47
 
49
48
  obs: null,
50
49
  attempts: 0,
50
+ poolWaitAttempts: 0,
51
+ __scrollBound: false,
51
52
  };
52
53
 
53
54
  function normalizeBool(v) {
@@ -143,18 +144,9 @@
143
144
  else window.ezstandalone.cmd.push(call);
144
145
  } catch (e) {}
145
146
  }
146
- } catch (e) {}
147
- };
148
- try {
149
- window.ezstandalone = window.ezstandalone || {};
150
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
151
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
152
- else window.ezstandalone.cmd.push(call);
153
- } catch (e) {}
154
- }
155
147
 
156
148
  function getRecyclable(liveArr) {
157
- const margin = 1800;
149
+ const margin = 600;
158
150
  for (let i = 0; i < liveArr.length; i++) {
159
151
  const entry = liveArr[i];
160
152
  if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
@@ -167,17 +159,6 @@
167
159
  return null;
168
160
  }
169
161
 
170
- function moveWrapAfter(wrap, target, kindClass, afterPos) {
171
- try {
172
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
173
- wrap.setAttribute('data-ezoic-after', String(afterPos));
174
- target.insertAdjacentElement('afterend', wrap);
175
- return true;
176
- } catch (e) {
177
- return false;
178
- }
179
- }
180
-
181
162
  function pickId(pool, liveArr) {
182
163
  if (pool.length) return { id: pool.shift(), recycled: null };
183
164
  const recycled = getRecyclable(liveArr);
@@ -245,30 +226,20 @@
245
226
  try {
246
227
  state.usedTopics.forEach((id) => ids.push(id));
247
228
  state.usedPosts.forEach((id) => ids.push(id));
248
- state.usedCategories && state.usedCategories.forEach((id) => ids.push(id));
229
+ state.usedCategories.forEach((id) => ids.push(id));
249
230
  } catch (e) {}
250
231
  destroyPlaceholderIds(ids);
251
232
  }
252
- } catch (e) {}
253
- };
254
-
255
- try {
256
- window.ezstandalone = window.ezstandalone || {};
257
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
258
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
259
- else window.ezstandalone.cmd.push(call);
260
- } catch (e) {}
261
- }
262
233
 
263
234
  function patchShowAds() {
264
235
  // Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
265
236
  try {
266
237
  window.ezstandalone = window.ezstandalone || {};
267
238
  const ez = window.ezstandalone;
268
- if (ez.__nodebbEzoicPatched) return;
239
+ if (window.__nodebbEzoicPatched) return;
269
240
  if (typeof ez.showAds !== 'function') return;
270
241
 
271
- ez.__nodebbEzoicPatched = true;
242
+ window.__nodebbEzoicPatched = true;
272
243
  const orig = ez.showAds;
273
244
 
274
245
  ez.showAds = function (arg) {
@@ -292,7 +263,7 @@
292
263
  function markFilled(wrap) {
293
264
  try {
294
265
  if (!wrap) return;
295
- try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
266
+ // Disconnect the fill observer first (no need to remove+re-add the attribute)
296
267
  try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
297
268
  wrap.setAttribute('data-ezoic-filled', '1');
298
269
  } catch (e) {}
@@ -339,8 +310,6 @@
339
310
  }
340
311
  return filled;
341
312
  }
342
- return filled;
343
- }
344
313
 
345
314
  function scheduleRefill(delay = 350) {
346
315
  clearTimeout(state.retryTimer);
@@ -364,24 +333,19 @@
364
333
  const id = state.retryQueue.shift();
365
334
  if (!id) {
366
335
  state.retryQueueRunning = false;
367
- state.badIds = new Set();
368
- state.definedIds = new Set();
369
336
  return;
370
337
  }
371
338
  state.retryQueueSet.delete(id);
372
- // If this id was previously attempted and still empty, force a full reset before re-requesting.
373
339
  const attempts = (state.retryById.get(id) || 0);
374
340
  const phNow = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
375
341
  const wrapNow = phNow && phNow.parentElement;
376
- // If Ezoic already defined this id earlier but the placeholder is empty now, we must destroy+reset before re-showAds.
377
- if (wrapNow && wrapNow.isConnected && state.definedIds && state.definedIds.has(id) && !isPlaceholderFilled(id) && !isWrapMarkedFilled(wrapNow)) {
378
- destroyPlaceholderIds([id]);
379
- resetPlaceholderInWrap(wrapNow, id);
380
- }
381
- // If this id was previously attempted and still empty, force a full reset before re-requesting.
382
- if (attempts > 0 && wrapNow && wrapNow.isConnected && !isPlaceholderFilled(id) && !isWrapMarkedFilled(wrapNow)) {
342
+ // Reset before re-requesting if Ezoic already defined this id but placeholder is now empty,
343
+ // OR if a previous attempt already failed.
344
+ if (wrapNow && wrapNow.isConnected && !isPlaceholderFilled(id) && !isWrapMarkedFilled(wrapNow) &&
345
+ (state.definedIds.has(id) || attempts > 0)) {
383
346
  destroyPlaceholderIds([id]);
384
347
  resetPlaceholderInWrap(wrapNow, id);
348
+ attachFillObserver(wrapNow, id);
385
349
  }
386
350
  callShowAdsWhenReady(id);
387
351
  setTimeout(step, 1100);
@@ -446,8 +410,11 @@
446
410
  return false;
447
411
  };
448
412
 
413
+ const startPageKey = state.pageKey;
449
414
  let attempts = 0;
450
415
  (function waitForPh() {
416
+ // Abort if the user navigated away since this showAds was scheduled
417
+ if (state.pageKey !== startPageKey) return;
451
418
  attempts += 1;
452
419
  const el = document.getElementById(phId);
453
420
  if (el && el.isConnected) {
@@ -470,6 +437,7 @@
470
437
 
471
438
  let tries = 0;
472
439
  (function tick() {
440
+ if (state.pageKey !== startPageKey) { state.pendingById.delete(id); return; }
473
441
  tries += 1;
474
442
  if (doCall() || tries >= 5) {
475
443
  if (tries >= 5) state.pendingById.delete(id);
@@ -484,30 +452,28 @@
484
452
  })();
485
453
  }
486
454
 
487
- function nextId(pool) {
488
- // backward compatible: the injector passes the pool array
489
- if (Array.isArray(pool) && pool.length) return pool.shift();
490
- return null;
491
- }
492
-
493
455
  async function fetchConfig() {
494
456
  if (state.cfg) return state.cfg;
495
457
  if (state.cfgPromise) return state.cfgPromise;
496
458
 
497
459
  state.cfgPromise = (async () => {
498
- try {
499
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
500
- if (!res.ok) return null;
501
- state.cfg = await res.json();
502
- return state.cfg;
503
- } catch (e) {
504
- return null;
505
- } finally {
506
- state.cfgPromise = null;
460
+ const MAX_TRIES = 3;
461
+ let delay = 800;
462
+ for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
463
+ try {
464
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
465
+ if (res.ok) {
466
+ state.cfg = await res.json();
467
+ return state.cfg;
468
+ }
469
+ } catch (e) {}
470
+ if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
471
+ delay *= 2;
507
472
  }
473
+ return null;
508
474
  })();
509
475
 
510
- return state.cfgPromise;
476
+ try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
511
477
  }
512
478
 
513
479
  function initPools(cfg) {
@@ -577,14 +543,16 @@
577
543
 
578
544
  liveArr.push({ id, wrap });
579
545
  // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
580
- if (wrap && (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))) {
546
+ if (wrap && (
547
+ (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) ||
548
+ (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
549
+ )) {
581
550
  try { wrap.remove(); } catch (e) {}
582
551
  // Put id back if it was newly consumed (not recycled)
583
552
  if (!(pick.recycled && pick.recycled.wrap)) {
584
553
  try { kindPool.unshift(id); } catch (e) {}
585
554
  try { usedSet.delete(id); } catch (e) {}
586
555
  }
587
- inserted -= 0; // no-op
588
556
  continue;
589
557
  }
590
558
  if (!(pick.recycled && pick.recycled.wrap)) {
@@ -615,19 +583,24 @@
615
583
  state.poolTopics = [];
616
584
  state.poolPosts = [];
617
585
  state.poolCategories = [];
618
- state.poolCategories = [];
619
586
  state.usedTopics.clear();
620
587
  state.usedPosts.clear();
621
- state.usedCategories && state.usedCategories.clear();
588
+ state.usedCategories.clear();
622
589
  state.liveBetween = [];
623
590
  state.liveMessage = [];
624
591
  state.liveCategory = [];
625
- state.usedCategories.clear();
626
592
 
627
593
  state.lastShowById = new Map();
628
594
  state.pendingById = new Set();
595
+ state.retryById = new Map();
596
+ state.retryQueue = [];
597
+ state.retryQueueSet = new Set();
598
+ state.retryQueueRunning = false;
599
+ state.badIds = new Set();
600
+ state.definedIds = new Set();
629
601
 
630
602
  state.attempts = 0;
603
+ state.poolWaitAttempts = 0;
631
604
 
632
605
  document.querySelectorAll(`.${WRAP_CLASS}`).forEach(el => el.remove());
633
606
 
@@ -635,13 +608,14 @@
635
608
  state.scheduled = false;
636
609
  clearTimeout(state.timer);
637
610
  state.timer = null;
611
+ clearTimeout(state.retryTimer);
612
+ state.retryTimer = null;
638
613
  }
639
614
 
640
615
  function ensureObserver() {
641
616
  if (state.obs) return;
642
617
  state.obs = new MutationObserver(() => scheduleRun('mutation'));
643
618
  try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
644
- setTimeout(() => { if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; } }, 15000);
645
619
  }
646
620
 
647
621
  async function runCore() {
@@ -660,7 +634,7 @@
660
634
  inserted = injectBetween('ezoic-ad-message', getPostContainers(),
661
635
  Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
662
636
  normalizeBool(cfg.showFirstMessageAd),
663
- 'message',
637
+ state.poolPosts,
664
638
  state.usedPosts);
665
639
  }
666
640
  } else if (kind === 'categoryTopics') {
@@ -668,7 +642,7 @@
668
642
  inserted = injectBetween('ezoic-ad-between', getTopicItems(),
669
643
  Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
670
644
  normalizeBool(cfg.showFirstTopicAd),
671
- 'between',
645
+ state.poolTopics,
672
646
  state.usedTopics);
673
647
  }
674
648
  } else if (kind === 'categories') {
@@ -676,7 +650,7 @@
676
650
  inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
677
651
  Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
678
652
  normalizeBool(cfg.showFirstCategoryAd),
679
- 'categories',
653
+ state.poolCategories,
680
654
  state.usedCategories);
681
655
  }
682
656
  }
@@ -696,7 +670,22 @@
696
670
  return;
697
671
  }
698
672
 
699
- if (inserted >= MAX_INSERTS_PER_RUN) setTimeout(() => scheduleRun('continue'), 140);
673
+ if (inserted >= MAX_INSERTS_PER_RUN) {
674
+ // Plus d'insertions possibles ce cycle, continuer immédiatement
675
+ setTimeout(() => scheduleRun('continue'), 140);
676
+ } else if (inserted === 0 && count > 0) {
677
+ // Pool épuisé ou recyclage pas encore disponible.
678
+ // Réessayer jusqu'à 8 fois (toutes les 400ms) pour laisser aux anciens wrappers
679
+ // le temps de défiler hors écran et devenir recyclables.
680
+ if (state.poolWaitAttempts < 8) {
681
+ state.poolWaitAttempts += 1;
682
+ setTimeout(() => scheduleRun('pool-wait'), 400);
683
+ } else {
684
+ state.poolWaitAttempts = 0;
685
+ }
686
+ } else if (inserted > 0) {
687
+ state.poolWaitAttempts = 0;
688
+ }
700
689
  }
701
690
 
702
691
  function scheduleRun() {
@@ -752,13 +741,6 @@
752
741
  });
753
742
  }
754
743
 
755
- cleanup();
756
- bind();
757
- ensureObserver();
758
- state.pageKey = getPageKey();
759
- scheduleRun();
760
- setTimeout(scheduleRun, 250);
761
- })()
762
744
  function bindScroll() {
763
745
  if (state.__scrollBound) return;
764
746
  state.__scrollBound = true;
@@ -768,10 +750,21 @@
768
750
  ticking = true;
769
751
  window.requestAnimationFrame(() => {
770
752
  ticking = false;
753
+ // Réinitialiser le compteur d'attente de recyclage à chaque reprise de scroll
754
+ state.poolWaitAttempts = 0;
771
755
  enforceNoAdjacentAds();
772
756
  scheduleRefill(200);
757
+ // Tenter aussi d'insérer de nouvelles pubs si de nouveaux items sont apparus
758
+ scheduleRun();
773
759
  });
774
760
  }, { passive: true });
775
761
  }
776
762
 
777
- ;
763
+ cleanup();
764
+ bind();
765
+ bindScroll();
766
+ ensureObserver();
767
+ state.pageKey = getPageKey();
768
+ scheduleRun();
769
+ setTimeout(scheduleRun, 250);
770
+ })();