nodebb-plugin-ezoic-infinite 1.8.78 → 1.8.79

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
@@ -60,7 +60,7 @@ async function getSettings() {
60
60
  if (_settingsCache && (t - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
61
61
 
62
62
  const s = await meta.settings.get(SETTINGS_KEY);
63
- _settingsCacheAt = Date.now();
63
+ _settingsCacheAt = t;
64
64
  _settingsCache = {
65
65
  enableBetweenAds: parseBool(s.enableBetweenAds, true),
66
66
  showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
@@ -93,10 +93,17 @@ async function isUserExcluded(uid, excludedGroups) {
93
93
 
94
94
  _excludeCache.set(key, { value, at: Date.now() });
95
95
  if (_excludeCache.size > 1000) {
96
+ const now = Date.now();
96
97
  let toDel = 100;
97
- for (const k of _excludeCache.keys()) {
98
- _excludeCache.delete(k);
99
- if (--toDel <= 0) break;
98
+ // Supprimer en priorité les entrées expirées, puis les plus anciennes
99
+ for (const [k, v] of _excludeCache) {
100
+ if (now - v.at >= EXCLUDE_TTL) { _excludeCache.delete(k); if (--toDel <= 0) break; }
101
+ }
102
+ if (toDel > 0) {
103
+ for (const k of _excludeCache.keys()) {
104
+ _excludeCache.delete(k);
105
+ if (--toDel <= 0) break;
106
+ }
100
107
  }
101
108
  }
102
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.78",
3
+ "version": "1.8.79",
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
@@ -93,8 +93,7 @@
93
93
  mutGuard: 0,
94
94
  blockedUntil: 0,
95
95
  inflight: 0,
96
- pending: [],
97
- pendingSet: new Set(),
96
+ pending: new Set(),
98
97
  runQueued: false,
99
98
  burstActive: false,
100
99
  burstDeadline: 0,
@@ -213,11 +212,11 @@
213
212
  // ── GC disconnected wraps ──────────────────────────────────────────────────
214
213
 
215
214
  function gcDisconnectedWraps() {
216
- for (const [key, w] of Array.from(S.wrapByKey)) {
215
+ for (const [key, w] of S.wrapByKey) {
217
216
  if (!w?.isConnected) S.wrapByKey.delete(key);
218
217
  }
219
- for (const [klass, set] of Array.from(S.wrapsByClass)) {
220
- for (const w of Array.from(set)) {
218
+ for (const [klass, set] of S.wrapsByClass) {
219
+ for (const w of set) {
221
220
  if (w?.isConnected) continue;
222
221
  set.delete(w);
223
222
  const id = parseInt(w.getAttribute?.(ATTR.WRAPID), 10);
@@ -268,13 +267,17 @@
268
267
  return true;
269
268
  }
270
269
 
270
+ function addWrapTimer(id, ms, fn) {
271
+ const timers = S.wrapTimers.get(id) || [];
272
+ timers.push(setTimeout(() => { try { fn(); } catch (_) {} }, ms));
273
+ S.wrapTimers.set(id, timers);
274
+ }
275
+
271
276
  function scheduleUncollapseChecks(wrap, id) {
272
277
  if (!wrap || !id) return;
273
- const ids = S.wrapTimers.get(id) || [];
274
278
  for (const ms of TIMING.UNCOLLAPSE_CHECK_MS) {
275
- ids.push(setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms));
279
+ addWrapTimer(id, ms, () => clearEmptyIfFilled(wrap));
276
280
  }
277
- S.wrapTimers.set(id, ids);
278
281
  }
279
282
 
280
283
  // ── Pool management ────────────────────────────────────────────────────────
@@ -401,12 +404,8 @@
401
404
  }
402
405
  const key = w.getAttribute(ATTR.ANCHOR);
403
406
  if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
404
- for (const cls of w.classList) {
405
- if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
406
- S.wrapsByClass.get(cls)?.delete(w);
407
- break;
408
- }
409
- }
407
+ const colonIdx = key?.indexOf(':') ?? -1;
408
+ if (colonIdx > 0) S.wrapsByClass.get(key.slice(0, colonIdx))?.delete(w);
410
409
  w.remove();
411
410
  } catch (_) {}
412
411
  }
@@ -505,7 +504,7 @@
505
504
  if (!id || isBlocked()) return;
506
505
  if (now() - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
507
506
  if (S.inflight >= MAX_INFLIGHT) {
508
- if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
507
+ S.pending.add(id);
509
508
  return;
510
509
  }
511
510
  startShow(id);
@@ -513,9 +512,9 @@
513
512
 
514
513
  function drainQueue() {
515
514
  if (isBlocked()) return;
516
- while (S.inflight < MAX_INFLIGHT && S.pending.length) {
517
- const id = S.pending.shift();
518
- S.pendingSet.delete(id);
515
+ while (S.inflight < MAX_INFLIGHT && S.pending.size) {
516
+ const id = S.pending.values().next().value;
517
+ S.pending.delete(id);
519
518
  startShow(id);
520
519
  }
521
520
  }
@@ -557,22 +556,18 @@
557
556
  }
558
557
 
559
558
  function scheduleEmptyCheck(id, showTs) {
560
- const ids = S.wrapTimers.get(id) || [];
561
559
  for (const delay of [TIMING.EMPTY_CHECK_MS_1, TIMING.EMPTY_CHECK_MS_2]) {
562
- ids.push(setTimeout(() => {
563
- try {
564
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
565
- const wrap = ph?.closest(`.${WRAP_CLASS}`);
566
- if (!wrap || !ph?.isConnected) return;
567
- if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
568
- if (clearEmptyIfFilled(wrap)) return;
569
- if (ph.querySelector('[id^="div-gpt-ad"]')) return;
570
- if (ph.offsetHeight > 10) return;
571
- wrap.classList.add('is-empty');
572
- } catch (_) {}
573
- }, delay));
560
+ addWrapTimer(id, delay, () => {
561
+ const ph = document.getElementById(`${PH_PREFIX}${id}`);
562
+ const wrap = ph?.closest(`.${WRAP_CLASS}`);
563
+ if (!wrap || !ph?.isConnected) return;
564
+ if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
565
+ if (clearEmptyIfFilled(wrap)) return;
566
+ if (ph.querySelector('[id^="div-gpt-ad"]')) return;
567
+ if (ph.offsetHeight > 10) return;
568
+ wrap.classList.add('is-empty');
569
+ });
574
570
  }
575
- S.wrapTimers.set(id, ids);
576
571
  }
577
572
 
578
573
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
@@ -692,8 +687,7 @@
692
687
  S.wrapsByClass.clear();
693
688
  S.kind = null;
694
689
  S.inflight = 0;
695
- S.pending = [];
696
- S.pendingSet.clear();
690
+ S.pending.clear();
697
691
  S.burstActive = false;
698
692
  S.runQueued = false;
699
693
  }
@@ -797,10 +791,9 @@
797
791
 
798
792
  // ── Console muting ─────────────────────────────────────────────────────────
799
793
 
800
- function muteConsole() {
801
- if (window.__nbbEzMuted) return;
802
- window.__nbbEzMuted = true;
803
- const MUTED = [
794
+ const MUTED_RE = (() => {
795
+ const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
796
+ return new RegExp([
804
797
  `with id ${PH_PREFIX}`,
805
798
  'adsbygoogle.push() error',
806
799
  'already been defined',
@@ -815,12 +808,17 @@
815
808
  'Error in custom getTCData',
816
809
  'no interstitial API',
817
810
  'JS-Enable should only',
818
- ];
811
+ ].map(esc).join('|'));
812
+ })();
813
+
814
+ function muteConsole() {
815
+ if (window.__nbbEzMuted) return;
816
+ window.__nbbEzMuted = true;
819
817
  for (const method of ['log', 'info', 'warn', 'error']) {
820
818
  const orig = console[method];
821
819
  if (typeof orig !== 'function') continue;
822
820
  console[method] = function (...args) {
823
- if (typeof args[0] === 'string') { for (const p of MUTED) if (args[0].includes(p)) return; }
821
+ if (typeof args[0] === 'string' && MUTED_RE.test(args[0])) return;
824
822
  return orig.apply(console, args);
825
823
  };
826
824
  }
@@ -863,10 +861,10 @@
863
861
  warmNetwork(); patchShowAds(); getIO(); ensureDomObserver();
864
862
  requestBurst();
865
863
  });
864
+ // action:ajaxify.contentLoaded et action:category.loaded ne passent pas par hooks → jQuery uniquement
866
865
  const burstEvts = [
867
- 'action:ajaxify.contentLoaded', 'action:posts.loaded',
868
- 'action:topics.loaded', 'action:categories.loaded',
869
- 'action:category.loaded', 'action:topic.loaded',
866
+ 'action:ajaxify.contentLoaded',
867
+ 'action:category.loaded',
870
868
  ].map(e => `${e}.nbbEzoic`).join(' ');
871
869
  $(window).on(burstEvts, () => { if (!isBlocked()) requestBurst(); });
872
870
  try {
package/CLAUDE.md DELETED
@@ -1,67 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- A NodeBB 4.x plugin that integrates Ezoic Infinite Ads with infinite scroll support. It injects Ezoic placeholder ads between topics, posts, and categories using a pool of placeholder IDs.
8
-
9
- ## Development Setup
10
-
11
- This plugin has no build step, no test suite, and no dependencies beyond NodeBB's core. To test it:
12
-
13
- 1. Copy/symlink the plugin directory into a NodeBB instance's `node_modules/nodebb-plugin-ezoic-infinite/`
14
- 2. Activate the plugin in NodeBB Admin > Extend > Plugins
15
- 3. Configure placeholder IDs at Admin > Plugins > Ezoic Infinite Ads
16
-
17
- To package for distribution:
18
- ```bash
19
- zip -r nodebb-plugin-onekite-ezoic.zip . --exclude "*.zip" --exclude ".git/*" --exclude "CLAUDE.md"
20
- ```
21
-
22
- ## Architecture
23
-
24
- ### Server-side (`library.js`)
25
- Single file exporting a `plugin` object with NodeBB hooks:
26
- - `filter:middleware.renderHeader` → `injectEzoicHead`: Injects Ezoic scripts + inline config into `<head>`. For excluded users, injects a stub that stubs `ezstandalone` without loading ad scripts.
27
- - `static:app.load` → `init`: Registers admin routes (`/admin/plugins/ezoic-infinite`) and a config API endpoint (`/api/plugins/ezoic-infinite/config`).
28
- - `filter:admin.header.build` → `addAdminNavigation`: Adds the plugin to the ACP nav.
29
- - `action:settings.set` → `onSettingsSet`: Invalidates the settings cache on save.
30
-
31
- Settings are cached for 30s. Group membership for ad exclusion is cached for 60s per uid+groups combination (up to 1000 entries).
32
-
33
- The inline config is serialized as `window.__nbbEzoicCfg` in the HTML head, so `client.js` can read it without an extra network request.
34
-
35
- ### Client-side (`public/client.js`)
36
- Self-contained IIFE with no external dependencies beyond jQuery (available globally in NodeBB) and the Ezoic `ezstandalone` API.
37
-
38
- **Page type detection** (`detectKind`): Determines if the current page is a topic, category topic list, or categories index, then selects the corresponding ad kind (`ezoic-ad-message`, `ezoic-ad-between`, `ezoic-ad-categories`).
39
-
40
- **Ad injection flow**:
41
- 1. `requestBurst()` → `scheduleRun()` → `runCore()` → `injectBetween()`
42
- 2. `injectBetween` iterates DOM items, checks ordinal position against the configured interval, inserts wrapper divs after qualifying elements using `insertAfter()`
43
- 3. Each wrapper contains an Ezoic placeholder `<div id="ezoic-pub-ad-placeholder-{id}">`
44
- 4. `IntersectionObserver` triggers `enqueueShow(id)` → `startShow(id)` → `ezstandalone.showAds(id)` when placeholders approach the viewport
45
-
46
- **Pool/recycling**: Placeholder IDs are organized into three pools (topics, posts, categories). When all IDs in a pool are in use, `recycleWrap` finds the oldest off-screen wrap, calls `destroyPlaceholders`, and reuses it for the new position.
47
-
48
- **State resets**: On `action:ajaxify.start` (SPA navigation), `cleanup()` destroys all wraps, resets all state. On `action:ajaxify.end`, re-initializes and triggers a burst.
49
-
50
- ### Admin UI
51
- - `public/templates/admin/plugins/ezoic-infinite.tpl`: NodeBB Benchpress template (French labels), uses `Settings.load`/`Settings.save` from NodeBB's ACP framework
52
- - `public/admin.js`: Minimal ACP script wiring the save button to NodeBB's `settings` module
53
- - `public/style.css`: Visual styles for the `.nodebb-ezoic-wrap` and `.is-empty` states
54
-
55
- ### Key Constants (client.js)
56
- - `WRAP_CLASS = 'nodebb-ezoic-wrap'`: CSS class on all injected wrappers
57
- - `PH_PREFIX = 'ezoic-pub-ad-placeholder-'`: Prefix for placeholder element IDs
58
- - `MAX_INSERTS_RUN = 6`: Max ad insertions per `runCore()` call
59
- - `MAX_INFLIGHT = 4`: Max concurrent `showAds()` calls
60
- - Three placeholder ID pools must not overlap between ad types
61
- - `S.wrapTimers: Map<id, timerId[]>` tracks all pending timers per wrap — cancelled in `dropWrap()` and `cleanup()`
62
- - `cleanup()` disconnects `S.domObs` and `S.io` so they are re-created fresh on next SPA page
63
-
64
- ### Important Constraints
65
- - `sa.min.js` must load **synchronously** (no `async` attribute) — loading it async causes "Timed out waiting for loadingStatus LOADING" because it may execute before the CMP is ready
66
- - Placeholder IDs must be distinct across the three pools (topics, posts, categories)
67
- - The `showAds()` function is monkey-patched to deduplicate calls and guard against disconnected placeholders
Binary file