nodebb-plugin-ezoic-infinite 1.8.98 → 1.9.0

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,36 +29,50 @@ 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
32
+ let _allGroupsCache = null;
33
+ let _allGroupsCacheAt = 0;
34
+ let _allGroupsInflight = null;
35
+ let _allGroupsGen = 0;
36
+ const ALL_GROUPS_TTL = 300_000; // 5 min — admin-only route
35
37
 
36
38
  async function getAllGroups() {
37
39
  const t = Date.now();
38
40
  if (_allGroupsCache && (t - _allGroupsCacheAt) < ALL_GROUPS_TTL) return _allGroupsCache;
39
- try {
40
- let names = await db.getSortedSetRange('groups:createtime', 0, -1);
41
- if (!names?.length) {
42
- names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
41
+ if (_allGroupsInflight) return _allGroupsInflight;
42
+ const gen = _allGroupsGen;
43
+ _allGroupsInflight = (async () => {
44
+ try {
45
+ let names = await db.getSortedSetRange('groups:createtime', 0, -1);
46
+ if (!names?.length) {
47
+ names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
48
+ }
49
+ const data = (await groups.getGroupsData(
50
+ (names || []).filter(name => !groups.isPrivilegeGroup(name))
51
+ ))
52
+ .filter(g => g?.name)
53
+ .sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
54
+ if (_allGroupsGen === gen) {
55
+ _allGroupsCache = data;
56
+ _allGroupsCacheAt = Date.now();
57
+ }
58
+ return _allGroupsCache || data;
59
+ } catch (err) {
60
+ console.error('[ezoic-infinite] getAllGroups error:', err.message);
61
+ return [];
62
+ } finally {
63
+ if (_allGroupsGen === gen) _allGroupsInflight = null;
43
64
  }
44
- _allGroupsCache = (await groups.getGroupsData(
45
- (names || []).filter(name => !groups.isPrivilegeGroup(name))
46
- ))
47
- .filter(g => g?.name)
48
- .sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
49
- _allGroupsCacheAt = Date.now();
50
- return _allGroupsCache;
51
- } catch (err) {
52
- console.error('[ezoic-infinite] getAllGroups error:', err.message);
53
- return [];
54
- }
65
+ })();
66
+ return _allGroupsInflight;
55
67
  }
56
68
 
57
69
  // ── Settings cache ───────────────────────────────────────────────────────────
58
70
 
59
- let _settingsCache = null;
60
- let _settingsCacheAt = 0;
61
- const SETTINGS_TTL = 30_000;
71
+ let _settingsCache = null;
72
+ let _settingsCacheAt = 0;
73
+ let _settingsInflight = null;
74
+ let _settingsGen = 0;
75
+ const SETTINGS_TTL = 30_000;
62
76
 
63
77
  const _excludeCache = new Map();
64
78
  const EXCLUDE_TTL = 60_000;
@@ -66,25 +80,35 @@ const EXCLUDE_TTL = 60_000;
66
80
  async function getSettings() {
67
81
  const t = Date.now();
68
82
  if (_settingsCache && (t - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
69
-
70
- const s = await meta.settings.get(SETTINGS_KEY);
71
- _settingsCacheAt = t;
72
- _settingsCache = {
73
- enableBetweenAds: parseBool(s.enableBetweenAds, true),
74
- showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
75
- placeholderIds: (s.placeholderIds || '').trim(),
76
- intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
77
- enableCategoryAds: parseBool(s.enableCategoryAds, false),
78
- showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
79
- categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
80
- intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
81
- enableMessageAds: parseBool(s.enableMessageAds, false),
82
- showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
83
- messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
84
- messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
85
- excludedGroups: normalizeExcludedGroups(s.excludedGroups),
86
- };
87
- return _settingsCache;
83
+ if (_settingsInflight) return _settingsInflight;
84
+ const gen = _settingsGen;
85
+ _settingsInflight = (async () => {
86
+ try {
87
+ const s = await meta.settings.get(SETTINGS_KEY);
88
+ if (_settingsGen === gen) {
89
+ _settingsCacheAt = Date.now();
90
+ _settingsCache = {
91
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
92
+ showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
93
+ placeholderIds: (s.placeholderIds || '').trim(),
94
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
95
+ enableCategoryAds: parseBool(s.enableCategoryAds, false),
96
+ showFirstCategoryAd: parseBool(s.showFirstCategoryAd, false),
97
+ categoryPlaceholderIds: (s.categoryPlaceholderIds || '').trim(),
98
+ intervalCategories: Math.max(1, parseInt(s.intervalCategories, 10) || 4),
99
+ enableMessageAds: parseBool(s.enableMessageAds, false),
100
+ showFirstMessageAd: parseBool(s.showFirstMessageAd, false),
101
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
102
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
103
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
104
+ };
105
+ }
106
+ return _settingsCache;
107
+ } finally {
108
+ if (_settingsGen === gen) _settingsInflight = null;
109
+ }
110
+ })();
111
+ return _settingsInflight;
88
112
  }
89
113
 
90
114
  async function isUserExcluded(uid, excludedGroups) {
@@ -115,13 +139,17 @@ let _inlineCfgNormal = null;
115
139
  let _inlineCfgExcluded = null;
116
140
 
117
141
  function clearCaches() {
118
- _settingsCache = null;
119
- _settingsCacheAt = 0;
142
+ _settingsGen++;
143
+ _settingsCache = null;
144
+ _settingsCacheAt = 0;
145
+ _settingsInflight = null;
120
146
  _excludeCache.clear();
121
147
  _inlineCfgNormal = null;
122
148
  _inlineCfgExcluded = null;
149
+ _allGroupsGen++;
123
150
  _allGroupsCache = null;
124
151
  _allGroupsCacheAt = 0;
152
+ _allGroupsInflight = null;
125
153
  }
126
154
 
127
155
  // ── Client config ────────────────────────────────────────────────────────────
@@ -215,6 +243,8 @@ plugin.injectEzoicHead = async (data) => {
215
243
  };
216
244
 
217
245
  plugin.init = async ({ router, middleware }) => {
246
+ getSettings().catch(() => {});
247
+
218
248
  async function render(req, res) {
219
249
  const settings = await getSettings();
220
250
  const allGroups = await getAllGroups();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.98",
3
+ "version": "1.9.0",
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
@@ -101,6 +101,7 @@
101
101
  burstDeadline: 0,
102
102
  burstCount: 0,
103
103
  lastBurstTs: 0,
104
+ cleaningUp: false,
104
105
  };
105
106
 
106
107
  const isBlocked = () => now() < S.blockedUntil;
@@ -121,10 +122,8 @@
121
122
 
122
123
  async function fetchConfig() {
123
124
  if (S.cfg) return S.cfg;
124
- try {
125
- const inline = window.__nbbEzoicCfg;
126
- if (inline && typeof inline === 'object') { S.cfg = inline; return S.cfg; }
127
- } catch (_) {}
125
+ const inline = window.__nbbEzoicCfg;
126
+ if (inline && typeof inline === 'object') { S.cfg = inline; return S.cfg; }
128
127
  try {
129
128
  const ctrl = new AbortController();
130
129
  const t = setTimeout(() => ctrl.abort(), 5_000);
@@ -205,14 +204,14 @@
205
204
  function getTopics() {
206
205
  const t = now();
207
206
  if (_topicsCache && t - _topicsCacheTs < POSTS_CACHE_MS) return _topicsCache;
208
- _topicsCache = document.querySelectorAll(SEL.topic);
207
+ _topicsCache = Array.from(document.querySelectorAll(SEL.topic));
209
208
  _topicsCacheTs = t;
210
209
  return _topicsCache;
211
210
  }
212
211
  function getCategories() {
213
212
  const t = now();
214
213
  if (_catsCache && t - _catsCacheTs < POSTS_CACHE_MS) return _catsCache;
215
- _catsCache = document.querySelectorAll(SEL.category);
214
+ _catsCache = Array.from(document.querySelectorAll(SEL.category));
216
215
  _catsCacheTs = t;
217
216
  return _catsCache;
218
217
  }
@@ -435,15 +434,19 @@
435
434
  if (ph instanceof Element) S.io?.unobserve(ph);
436
435
  const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
437
436
  if (Number.isFinite(id)) {
438
- S.mountedIds.delete(id);
439
- S.lastShow.delete(id);
440
437
  const timers = S.wrapTimers.get(id);
441
438
  if (timers) { for (const t of timers) clearTimeout(t); S.wrapTimers.delete(id); }
439
+ if (!S.cleaningUp) {
440
+ S.mountedIds.delete(id);
441
+ S.lastShow.delete(id);
442
+ }
443
+ }
444
+ if (!S.cleaningUp) {
445
+ const key = w.getAttribute(ATTR.ANCHOR);
446
+ if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
447
+ const colonIdx = key?.indexOf(':') ?? -1;
448
+ if (colonIdx > 0) S.wrapsByClass.get(key.slice(0, colonIdx))?.delete(w);
442
449
  }
443
- const key = w.getAttribute(ATTR.ANCHOR);
444
- if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
445
- const colonIdx = key?.indexOf(':') ?? -1;
446
- if (colonIdx > 0) S.wrapsByClass.get(key.slice(0, colonIdx))?.delete(w);
447
450
  w.remove();
448
451
  } catch (_) {}
449
452
  }
@@ -722,9 +725,12 @@
722
725
  const ez = window.ezstandalone;
723
726
  if (typeof ez?.destroyPlaceholders === 'function') ez.destroyPlaceholders();
724
727
  } catch (_) {}
728
+ S.cleaningUp = true;
725
729
  mutate(() => {
726
730
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
727
731
  });
732
+ S.cleaningUp = false;
733
+ // Safety net: wrapTimers should be empty after dropWrap loop, but clear any orphans.
728
734
  for (const timers of S.wrapTimers.values()) { for (const t of timers) clearTimeout(t); }
729
735
  S.wrapTimers.clear();
730
736
  S.domObs?.disconnect(); S.domObs = null;