nodebb-plugin-ezoic-infinite 1.8.94 → 1.8.96

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
@@ -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 [];
@@ -108,10 +116,17 @@ async function isUserExcluded(uid, excludedGroups) {
108
116
  return value;
109
117
  }
110
118
 
119
+ let _inlineCfgNormal = null;
120
+ let _inlineCfgExcluded = null;
121
+
111
122
  function clearCaches() {
112
123
  _settingsCache = null;
113
124
  _settingsCacheAt = 0;
114
125
  _excludeCache.clear();
126
+ _inlineCfgNormal = null;
127
+ _inlineCfgExcluded = null;
128
+ _allGroupsCache = null;
129
+ _allGroupsCacheAt = 0;
115
130
  }
116
131
 
117
132
  // ── Client config ────────────────────────────────────────────────────────────
@@ -176,8 +191,12 @@ plugin.injectEzoicHead = async (data) => {
176
191
  const uid = data.req?.uid ?? 0;
177
192
  const excluded = await isUserExcluded(uid, settings.excludedGroups);
178
193
 
194
+ if (!_inlineCfgNormal) {
195
+ _inlineCfgNormal = serializeInlineConfig(buildClientConfig(settings, false));
196
+ _inlineCfgExcluded = serializeInlineConfig(buildClientConfig(settings, true));
197
+ }
198
+
179
199
  if (excluded) {
180
- const cfg = buildClientConfig(settings, true);
181
200
  const stub = '<script data-cfasync="false">'
182
201
  + 'window._ezaq=window._ezaq||{};'
183
202
  + 'window.ezstandalone=window.ezstandalone||{};'
@@ -185,14 +204,13 @@ plugin.injectEzoicHead = async (data) => {
185
204
  + '</script>';
186
205
  data.templateData.customHTML =
187
206
  stub + '\n' +
188
- serializeInlineConfig(cfg) +
207
+ _inlineCfgExcluded +
189
208
  (data.templateData.customHTML || '');
190
209
  } else {
191
- const cfg = buildClientConfig(settings, false);
192
210
  data.templateData.customHTML =
193
211
  HEAD_PRECONNECTS + '\n' +
194
212
  EZOIC_SCRIPTS + '\n' +
195
- serializeInlineConfig(cfg) +
213
+ _inlineCfgNormal +
196
214
  (data.templateData.customHTML || '');
197
215
  }
198
216
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.94",
3
+ "version": "1.8.96",
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,11 +1,12 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v2.5.1
2
+ * NodeBB Ezoic Infinite Ads — client.js v2.5.2
3
3
  *
4
4
  * Architecture: proven v50 core + targeted improvements.
5
5
  * Ezoic API: showAds() + destroyPlaceholders() per official docs.
6
6
  * Key features: O(1) recycle via wrapsByClass, MutationObserver fill detect,
7
7
  * conservative empty check, aria-hidden + TCF protection, retry boot for
8
8
  * Cloudflare/async timing.
9
+ * v2.5.2: removed redundant warmNetwork() — preconnects are server-injected.
9
10
  */
10
11
  (function nbbEzoicInfinite() {
11
12
  'use strict';
@@ -80,6 +81,7 @@
80
81
  pageKey: null,
81
82
  kind: null,
82
83
  cfg: null,
84
+ opts: null,
83
85
  poolsReady: false,
84
86
  pools: { topics: [], posts: [], categories: [] },
85
87
  cursors: { topics: 0, posts: 0, categories: 0 },
@@ -131,6 +133,17 @@
131
133
  S.pools.topics = parseIds(cfg.placeholderIds);
132
134
  S.pools.posts = parseIds(cfg.messagePlaceholderIds);
133
135
  S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
136
+ S.opts = {
137
+ enableBetweenAds: normBool(cfg.enableBetweenAds),
138
+ showFirstTopicAd: normBool(cfg.showFirstTopicAd),
139
+ intervalTopics: Math.max(1, parseInt(cfg.intervalPosts, 10) || 3),
140
+ enableCategoryAds: normBool(cfg.enableCategoryAds),
141
+ showFirstCategoryAd: normBool(cfg.showFirstCategoryAd),
142
+ intervalCategories: Math.max(1, parseInt(cfg.intervalCategories, 10) || 3),
143
+ enableMessageAds: normBool(cfg.enableMessageAds),
144
+ showFirstMessageAd: normBool(cfg.showFirstMessageAd),
145
+ messageInterval: Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
146
+ };
134
147
  S.poolsReady = true;
135
148
  }
136
149
 
@@ -168,6 +181,7 @@
168
181
  for (let i = 0; i < all.length; i++) {
169
182
  const el = all[i];
170
183
  if (!el.isConnected) continue;
184
+ if (el.childElementCount === 0) continue;
171
185
  if (!el.querySelector('[component="post/content"]')) continue;
172
186
  const parent = el.parentElement?.closest(SEL.post);
173
187
  if (parent && parent !== el) continue;
@@ -620,18 +634,19 @@
620
634
  const kind = getKind();
621
635
  if (kind === 'other') return 0;
622
636
 
623
- const exec = (klass, getItems, cfgEnable, cfgInterval, cfgShowFirst, poolKey) => {
624
- if (!normBool(cfgEnable)) return 0;
625
- return injectBetween(klass, getItems(), Math.max(1, parseInt(cfgInterval, 10) || 3), normBool(cfgShowFirst), poolKey);
637
+ const exec = (klass, getItems, enable, interval, showFirst, poolKey) => {
638
+ if (!enable) return 0;
639
+ return injectBetween(klass, getItems(), interval, showFirst, poolKey);
626
640
  };
627
641
 
642
+ const o = S.opts;
628
643
  if (kind === 'topic')
629
- return exec('ezoic-ad-message', getPosts, cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
644
+ return exec('ezoic-ad-message', getPosts, o.enableMessageAds, o.messageInterval, o.showFirstMessageAd, 'posts');
630
645
  if (kind === 'categoryTopics') {
631
646
  pruneOrphansBetween();
632
- return exec('ezoic-ad-between', getTopics, cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
647
+ return exec('ezoic-ad-between', getTopics, o.enableBetweenAds, o.intervalTopics, o.showFirstTopicAd, 'topics');
633
648
  }
634
- return exec('ezoic-ad-categories', getCategories, cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
649
+ return exec('ezoic-ad-categories', getCategories, o.enableCategoryAds, o.intervalCategories, o.showFirstCategoryAd, 'categories');
635
650
  }
636
651
 
637
652
  // ── Scheduler ──────────────────────────────────────────────────────────────
@@ -687,6 +702,7 @@
687
702
  S.domObs?.disconnect(); S.domObs = null;
688
703
  S.io?.disconnect(); S.io = null;
689
704
  S.cfg = null;
705
+ S.opts = null;
690
706
  S.poolsReady = false;
691
707
  S.pools = { topics: [], posts: [], categories: [] };
692
708
  S.cursors = { topics: 0, posts: 0, categories: 0 };
@@ -727,16 +743,21 @@
727
743
  for (const node of m.addedNodes) {
728
744
  if (!(node instanceof Element)) continue;
729
745
  try {
730
- if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
731
- const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) || m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
732
- if (wrap) clearEmptyIfFilled(wrap);
746
+ // Check closest first (cheap) before doing FILL_SEL querySelector on every added node
747
+ const emptyWrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ?? m.target?.closest?.(`.${WRAP_CLASS}.is-empty`);
748
+ if (emptyWrap && (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL))) {
749
+ clearEmptyIfFilled(emptyWrap);
733
750
  }
734
751
  } catch (_) {}
735
752
  try {
736
- const reinserted = node.classList?.contains(WRAP_CLASS) ? [node] : Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
737
- for (const wrap of reinserted) {
738
- const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
739
- if (id > 0) { const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
753
+ if (node.classList?.contains(WRAP_CLASS)) {
754
+ const id = parseInt(node.getAttribute(ATTR.WRAPID), 10);
755
+ if (id > 0) { const ph = node.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
756
+ } else if (node.childElementCount > 0) {
757
+ for (const wrap of node.querySelectorAll(`.${WRAP_CLASS}`)) {
758
+ const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
759
+ if (id > 0) { const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`); if (ph) try { getIO()?.observe(ph); } catch (_) {} }
760
+ }
740
761
  }
741
762
  } catch (_) {}
742
763
  if (!needsBurst) {
@@ -798,30 +819,6 @@
798
819
  try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
799
820
  }
800
821
 
801
- // ── Network warmup ─────────────────────────────────────────────────────────
802
-
803
- let _warmed = false;
804
- function warmNetwork() {
805
- if (_warmed) return;
806
- _warmed = true;
807
- const head = document.head;
808
- if (!head) return;
809
- for (const [rel, href, cors] of [
810
- ['preconnect', 'https://g.ezoic.net', true ],
811
- ['preconnect', 'https://go.ezoic.net', true ],
812
- ['preconnect', 'https://securepubads.g.doubleclick.net', true ],
813
- ['preconnect', 'https://pagead2.googlesyndication.com', true ],
814
- ['dns-prefetch', 'https://g.ezoic.net', false],
815
- ['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
816
- ]) {
817
- if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
818
- const l = document.createElement('link');
819
- l.rel = rel; l.href = href;
820
- if (cors) l.crossOrigin = 'anonymous';
821
- head.appendChild(l);
822
- }
823
- }
824
-
825
822
  // ── Bindings ───────────────────────────────────────────────────────────────
826
823
 
827
824
  function bindNodeBB() {
@@ -832,7 +829,7 @@
832
829
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
833
830
  S.pageKey = pageKey(); S.kind = null; S.blockedUntil = 0;
834
831
  ensureTcfLocator(); protectAriaHidden();
835
- warmNetwork(); patchShowAds(); getIO(); ensureDomObserver();
832
+ patchShowAds(); getIO(); ensureDomObserver();
836
833
  requestBurst();
837
834
  });
838
835
  // action:ajaxify.contentLoaded et action:category.loaded ne passent pas par hooks → jQuery uniquement
@@ -863,7 +860,6 @@
863
860
  S.pageKey = pageKey();
864
861
  ensureTcfLocator();
865
862
  protectAriaHidden();
866
- warmNetwork();
867
863
  patchShowAds();
868
864
  getIO();
869
865
  ensureDomObserver();
@@ -890,6 +886,8 @@
890
886
  function retryBoot() {
891
887
  if (RETRY.count >= 12) return;
892
888
  RETRY.count++;
889
+ // On pages that never show ads, no need to retry (unless mid-reload recovery)
890
+ if (!RETRY.scriptReloaded && getKind() === 'other') return;
893
891
  patchShowAds();
894
892
 
895
893
  const ez = window.ezstandalone;