nodebb-plugin-ezoic-infinite 1.8.95 → 1.8.97

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.
@@ -4,7 +4,10 @@
4
4
  "WebFetch(domain:registry.npmjs.org)",
5
5
  "Bash(curl -sL \"https://registry.npmjs.org/nodebb-plugin-ezoic-infinite/-/nodebb-plugin-ezoic-infinite-1.8.81.tgz\" -o /tmp/ezoic-1.8.81.tgz && tar -tzf /tmp/ezoic-1.8.81.tgz)",
6
6
  "Bash(tar -xzf /tmp/ezoic-1.8.81.tgz -C /tmp/ package/library.js package/public/client.js package/package.json)",
7
- "Bash(cd /tmp && curl -sL \"https://registry.npmjs.org/nodebb-plugin-ezoic-infinite/-/nodebb-plugin-ezoic-infinite-1.8.81.tgz\" -o ezoic-1.8.81.tgz && tar -xzf ezoic-1.8.81.tgz && echo \"Extracted OK\")"
7
+ "Bash(cd /tmp && curl -sL \"https://registry.npmjs.org/nodebb-plugin-ezoic-infinite/-/nodebb-plugin-ezoic-infinite-1.8.81.tgz\" -o ezoic-1.8.81.tgz && tar -xzf ezoic-1.8.81.tgz && echo \"Extracted OK\")",
8
+ "Bash(node -e \"require\\(''./library.js''\\)\")",
9
+ "Bash(node --check \"C:\\\\Users\\\\arnau\\\\OneDrive\\\\Dev\\\\nodebb-plugin-onekite-ezoic\\\\public\\\\client.js\")",
10
+ "Bash(node --check \"C:\\\\Users\\\\arnau\\\\OneDrive\\\\Dev\\\\nodebb-plugin-onekite-ezoic\\\\library.js\")"
8
11
  ]
9
12
  }
10
13
  }
package/library.js CHANGED
@@ -29,17 +29,25 @@ function parseBool(v, def = false) {
29
29
  return s === '1' || s === 'true' || s === 'on' || s === 'yes';
30
30
  }
31
31
 
32
+ let _allGroupsCache = null;
33
+ let _allGroupsCacheAt = 0;
34
+ const ALL_GROUPS_TTL = 300_000; // 5 min — admin-only route
35
+
32
36
  async function getAllGroups() {
37
+ const t = Date.now();
38
+ if (_allGroupsCache && (t - _allGroupsCacheAt) < ALL_GROUPS_TTL) return _allGroupsCache;
33
39
  try {
34
40
  let names = await db.getSortedSetRange('groups:createtime', 0, -1);
35
41
  if (!names?.length) {
36
42
  names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
37
43
  }
38
- return (await groups.getGroupsData(
44
+ _allGroupsCache = (await groups.getGroupsData(
39
45
  (names || []).filter(name => !groups.isPrivilegeGroup(name))
40
46
  ))
41
47
  .filter(g => g?.name)
42
48
  .sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
49
+ _allGroupsCacheAt = Date.now();
50
+ return _allGroupsCache;
43
51
  } catch (err) {
44
52
  console.error('[ezoic-infinite] getAllGroups error:', err.message);
45
53
  return [];
@@ -93,15 +101,10 @@ async function isUserExcluded(uid, excludedGroups) {
93
101
 
94
102
  _excludeCache.set(key, { value, at: Date.now() });
95
103
  if (_excludeCache.size > 1000) {
96
- let toDel = 100;
97
- const t = Date.now();
98
- for (const [k, v] of _excludeCache) {
99
- if (!toDel) break;
100
- if (t - v.at >= EXCLUDE_TTL) { _excludeCache.delete(k); toDel--; }
101
- }
104
+ let n = 100;
102
105
  for (const k of _excludeCache.keys()) {
103
- if (!toDel) break;
104
- _excludeCache.delete(k); toDel--;
106
+ if (!n--) break;
107
+ _excludeCache.delete(k);
105
108
  }
106
109
  }
107
110
 
@@ -117,6 +120,8 @@ function clearCaches() {
117
120
  _excludeCache.clear();
118
121
  _inlineCfgNormal = null;
119
122
  _inlineCfgExcluded = null;
123
+ _allGroupsCache = null;
124
+ _allGroupsCacheAt = 0;
120
125
  }
121
126
 
122
127
  // ── Client config ────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.95",
3
+ "version": "1.8.97",
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
@@ -34,7 +34,7 @@
34
34
  SHOW_TIMEOUT_MS: 4_500,
35
35
  SHOW_RELEASE_MS: 700,
36
36
  RECYCLE_DELAY_MS: 300,
37
- UNCOLLAPSE_CHECK_MS: [500, 3_000, 10_000],
37
+ UNCOLLAPSE_CHECK_MS: [500, 5_000],
38
38
  };
39
39
 
40
40
  const MAX_INSERTS_RUN = 6;
@@ -81,6 +81,7 @@
81
81
  pageKey: null,
82
82
  kind: null,
83
83
  cfg: null,
84
+ opts: null,
84
85
  poolsReady: false,
85
86
  pools: { topics: [], posts: [], categories: [] },
86
87
  cursors: { topics: 0, posts: 0, categories: 0 },
@@ -104,6 +105,12 @@
104
105
 
105
106
  const isBlocked = () => now() < S.blockedUntil;
106
107
 
108
+ // ── Posts cache (burst-scoped) ─────────────────────────────────────────────
109
+
110
+ let _postsCache = null;
111
+ let _postsCacheTs = 0;
112
+ const POSTS_CACHE_MS = 200;
113
+
107
114
  function mutate(fn) {
108
115
  S.mutGuard++;
109
116
  try { fn(); } finally { S.mutGuard--; }
@@ -132,6 +139,17 @@
132
139
  S.pools.topics = parseIds(cfg.placeholderIds);
133
140
  S.pools.posts = parseIds(cfg.messagePlaceholderIds);
134
141
  S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
142
+ S.opts = {
143
+ enableBetweenAds: normBool(cfg.enableBetweenAds),
144
+ showFirstTopicAd: normBool(cfg.showFirstTopicAd),
145
+ intervalTopics: Math.max(1, parseInt(cfg.intervalPosts, 10) || 3),
146
+ enableCategoryAds: normBool(cfg.enableCategoryAds),
147
+ showFirstCategoryAd: normBool(cfg.showFirstCategoryAd),
148
+ intervalCategories: Math.max(1, parseInt(cfg.intervalCategories, 10) || 3),
149
+ enableMessageAds: normBool(cfg.enableMessageAds),
150
+ showFirstMessageAd: normBool(cfg.showFirstMessageAd),
151
+ messageInterval: Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
152
+ };
135
153
  S.poolsReady = true;
136
154
  }
137
155
 
@@ -164,17 +182,22 @@
164
182
  // ── DOM queries ────────────────────────────────────────────────────────────
165
183
 
166
184
  function getPosts() {
185
+ const t = now();
186
+ if (_postsCache && t - _postsCacheTs < POSTS_CACHE_MS) return _postsCache;
167
187
  const all = document.querySelectorAll(SEL.post);
168
188
  const out = [];
169
189
  for (let i = 0; i < all.length; i++) {
170
190
  const el = all[i];
171
191
  if (!el.isConnected) continue;
192
+ if (el.childElementCount === 0) continue;
172
193
  if (!el.querySelector('[component="post/content"]')) continue;
173
194
  const parent = el.parentElement?.closest(SEL.post);
174
195
  if (parent && parent !== el) continue;
175
196
  if (el.getAttribute('component') === 'post/parent') continue;
176
197
  out.push(el);
177
198
  }
199
+ _postsCache = out;
200
+ _postsCacheTs = t;
178
201
  return out;
179
202
  }
180
203
 
@@ -243,17 +266,14 @@
243
266
  const cfg = KIND[klass];
244
267
  if (!cfg) return false;
245
268
  const parent = wrap.parentElement;
246
- if (parent) {
247
- const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
248
- for (const sib of parent.children) {
249
- if (sib !== wrap) {
250
- try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
251
- }
269
+ if (!parent) return false;
270
+ const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
271
+ for (const sib of parent.children) {
272
+ if (sib !== wrap) {
273
+ try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
252
274
  }
253
275
  }
254
- try {
255
- return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
256
- } catch (_) { return false; }
276
+ return false;
257
277
  }
258
278
 
259
279
  const adjacentWrap = el => wrapIsLive(el.nextElementSibling) || wrapIsLive(el.previousElementSibling);
@@ -476,9 +496,12 @@
476
496
 
477
497
  // ── IntersectionObserver ───────────────────────────────────────────────────
478
498
 
499
+ let _ioMobile = null;
500
+
479
501
  function getIO() {
480
502
  if (S.io) return S.io;
481
503
  try {
504
+ _ioMobile = isMobile();
482
505
  S.io = new IntersectionObserver(entries => {
483
506
  for (const e of entries) {
484
507
  if (!e.isIntersecting) continue;
@@ -488,7 +511,7 @@
488
511
  }
489
512
  }, {
490
513
  root: null,
491
- rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP,
514
+ rootMargin: _ioMobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP,
492
515
  threshold: 0,
493
516
  });
494
517
  } catch (_) { S.io = null; }
@@ -621,18 +644,19 @@
621
644
  const kind = getKind();
622
645
  if (kind === 'other') return 0;
623
646
 
624
- const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
625
- if (!normBool(cfgEnable)) return 0;
626
- return injectBetween(klass, getItems(), Math.max(1, parseInt(cfgInterval, 10) || 3), normBool(cfgShowFirst), poolKey);
647
+ const exec = (klass, getItems, enable, interval, showFirst, poolKey) => {
648
+ if (!enable) return 0;
649
+ return injectBetween(klass, getItems(), interval, showFirst, poolKey);
627
650
  };
628
651
 
652
+ const o = S.opts;
629
653
  if (kind === 'topic')
630
- return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
654
+ return exec('ezoic-ad-message', getPosts, o.enableMessageAds, o.messageInterval, o.showFirstMessageAd, 'posts');
631
655
  if (kind === 'categoryTopics') {
632
656
  pruneOrphansBetween();
633
- return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
657
+ return exec('ezoic-ad-between', getTopics, o.enableBetweenAds, o.intervalTopics, o.showFirstTopicAd, 'topics');
634
658
  }
635
- return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
659
+ return exec('ezoic-ad-categories', getCategories, o.enableCategoryAds, o.intervalCategories, o.showFirstCategoryAd, 'categories');
636
660
  }
637
661
 
638
662
  // ── Scheduler ──────────────────────────────────────────────────────────────
@@ -686,8 +710,10 @@
686
710
  for (const timers of S.wrapTimers.values()) { for (const t of timers) clearTimeout(t); }
687
711
  S.wrapTimers.clear();
688
712
  S.domObs?.disconnect(); S.domObs = null;
689
- S.io?.disconnect(); S.io = null;
713
+ S.io?.disconnect(); S.io = null; _ioMobile = null;
714
+ _postsCache = null; _postsCacheTs = 0;
690
715
  S.cfg = null;
716
+ S.opts = null;
691
717
  S.poolsReady = false;
692
718
  S.pools = { topics: [], posts: [], categories: [] };
693
719
  S.cursors = { topics: 0, posts: 0, categories: 0 };
@@ -728,16 +754,21 @@
728
754
  for (const node of m.addedNodes) {
729
755
  if (!(node instanceof Element)) continue;
730
756
  try {
731
- if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
732
- const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) || m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
733
- if (wrap) clearEmptyIfFilled(wrap);
757
+ // Check closest first (cheap) before doing FILL_SEL querySelector on every added node
758
+ const emptyWrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ?? m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
759
+ if (emptyWrap && (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL))) {
760
+ clearEmptyIfFilled(emptyWrap);
734
761
  }
735
762
  } catch (_) {}
736
763
  try {
737
- const reinserted = node.classList?.contains(WRAP_CLASS) ? [node] : Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
738
- for (const wrap of reinserted) {
739
- const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
740
- if (id > 0) { const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
764
+ if (node.classList?.contains(WRAP_CLASS)) {
765
+ const id = parseInt(node.getAttribute(ATTR.WRAPID), 10);
766
+ if (id > 0) { const ph = node.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
767
+ } else if (node.childElementCount > 0) {
768
+ for (const wrap of node.querySelectorAll(`.${WRAP_CLASS}`)) {
769
+ const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
770
+ if (id > 0) { const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
771
+ }
741
772
  }
742
773
  } catch (_) {}
743
774
  if (!needsBurst) {
@@ -747,9 +778,12 @@
747
778
  }
748
779
  }
749
780
  }
750
- if (needsBurst) requestBurst();
781
+ if (needsBurst) { _postsCacheTs = 0; requestBurst(); }
751
782
  });
752
- try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
783
+ try {
784
+ const target = document.getElementById('content') || document.body;
785
+ S.domObs.observe(target, { childList: true, subtree: true });
786
+ } catch (_) {}
753
787
  }
754
788
 
755
789
  // ── TCF / CMP Protection ───────────────────────────────────────────────────
@@ -810,6 +844,8 @@
810
844
  S.pageKey = pageKey(); S.kind = null; S.blockedUntil = 0;
811
845
  ensureTcfLocator(); protectAriaHidden();
812
846
  patchShowAds(); getIO(); ensureDomObserver();
847
+ RETRY.count = 0; RETRY.scriptReloaded = false; RETRY.postReloadShown = false; RETRY.gen++;
848
+ setTimeout(() => retryBoot(RETRY.gen), 250);
813
849
  requestBurst();
814
850
  });
815
851
  // action:ajaxify.contentLoaded et action:category.loaded ne passent pas par hooks → jQuery uniquement
@@ -821,7 +857,7 @@
821
857
  try {
822
858
  require(['hooks'], hooks => {
823
859
  if (typeof hooks?.on !== 'function') return;
824
- for (const ev of ['action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded', 'action:topic.loaded'])
860
+ for (const ev of ['action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded', 'action:topic.loaded'])
825
861
  try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
826
862
  });
827
863
  } catch (_) {}
@@ -835,6 +871,21 @@
835
871
  }, { passive: true });
836
872
  }
837
873
 
874
+ function bindResize() {
875
+ let timer = null;
876
+ window.addEventListener('resize', () => {
877
+ clearTimeout(timer);
878
+ timer = setTimeout(() => {
879
+ if (!S.io || _ioMobile === null) return;
880
+ const m = isMobile();
881
+ if (m === _ioMobile) return;
882
+ S.io.disconnect(); S.io = null; _ioMobile = null;
883
+ getIO();
884
+ for (const id of S.mountedIds) observePh(id);
885
+ }, 250);
886
+ }, { passive: true });
887
+ }
888
+
838
889
  // ── Boot ───────────────────────────────────────────────────────────────────
839
890
 
840
891
  S.pageKey = pageKey();
@@ -845,6 +896,7 @@
845
896
  ensureDomObserver();
846
897
  bindNodeBB();
847
898
  bindScroll();
899
+ bindResize();
848
900
  S.blockedUntil = 0;
849
901
  if (document.readyState === 'complete') {
850
902
  requestBurst();
@@ -862,10 +914,13 @@
862
914
  // in ez-standalone.js → onStandaloneLoadEvent crash). After ~6s, remove and reload
863
915
  // sa.min.js; Ezoic's partial first-run state may let the second run succeed.
864
916
  // Once recovered, re-enqueue all mounted placeholders so showAds() fires.
865
- const RETRY = { count: 0, scriptReloaded: false, postReloadShown: false };
866
- function retryBoot() {
917
+ const RETRY = { count: 0, scriptReloaded: false, postReloadShown: false, gen: 0 };
918
+ function retryBoot(gen) {
919
+ if (gen !== RETRY.gen) return;
867
920
  if (RETRY.count >= 12) return;
868
921
  RETRY.count++;
922
+ // On pages that never show ads, no need to retry (unless mid-reload recovery)
923
+ if (!RETRY.scriptReloaded && getKind() === 'other') return;
869
924
  patchShowAds();
870
925
 
871
926
  const ez = window.ezstandalone;
@@ -916,8 +971,8 @@
916
971
  S.lastBurstTs = now() - TIMING.BURST_COOLDOWN_MS;
917
972
  requestBurst();
918
973
  }
919
- setTimeout(retryBoot, RETRY.count <= 4 ? 300 : 1000);
974
+ setTimeout(() => retryBoot(gen), RETRY.count <= 4 ? 300 : 1000);
920
975
  }
921
- setTimeout(retryBoot, 250);
976
+ setTimeout(() => retryBoot(RETRY.gen), 250);
922
977
 
923
978
  })();