nodebb-plugin-ezoic-infinite 1.5.76 → 1.5.78

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +62 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.76",
3
+ "version": "1.5.78",
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
@@ -10,7 +10,10 @@
10
10
  const POOL_ID = 'nodebb-ezoic-placeholder-pool';
11
11
 
12
12
  // Smoothness caps
13
- const MAX_INSERTS_PER_RUN = 3;
13
+ // Limit how many placements we inject per scan pass.
14
+ // Too low = you end up with only a handful of placeholders after ajaxify.
15
+ // Too high = jank on very long pages.
16
+ const MAX_INSERTS_PER_RUN = 8;
14
17
 
15
18
  // Keep empty (unfilled) wraps alive for a while. Topics/messages can fill late (auction/CMP).
16
19
  // Pruning too early makes ads look like they "disappear" while scrolling.
@@ -135,11 +138,8 @@ function tightenEzoicMinHeight(wrap) {
135
138
  }
136
139
  if (!maxBottom) return;
137
140
 
138
- // Account for the Ezoic badge (reportline) which is absolutely positioned.
139
- const report = refNode.querySelector('.reportline');
140
- if (report && report.offsetHeight) {
141
- maxBottom += report.offsetHeight;
142
- }
141
+ // NOTE: Do NOT add the Ezoic badge (reportline) height here.
142
+ // It is absolutely positioned and should not reserve layout space.
143
143
 
144
144
  const h = Math.max(1, Math.ceil(maxBottom));
145
145
 
@@ -229,6 +229,9 @@ function globalGapFixInit() {
229
229
  if (window.__nodebbEzoicGapFix) return;
230
230
  window.__nodebbEzoicGapFix = true;
231
231
 
232
+ // Observe only the main content area to minimize overhead.
233
+ const root = document.getElementById('content') || document.querySelector('[component="content"], #panel') || document.body;
234
+
232
235
  const inPostArea = (el) => {
233
236
  try {
234
237
  return !!(el && el.closest && el.closest('[component="post"], .topic, .posts, [component="topic"]'));
@@ -249,21 +252,37 @@ function globalGapFixInit() {
249
252
  });
250
253
  };
251
254
 
252
- requestAnimationFrame(() => maybeFix(document));
255
+ requestAnimationFrame(() => maybeFix(root));
256
+
257
+ // Batch DOM mutation processing into a single rAF to avoid doing work per mutation.
258
+ const pending = new Set();
259
+ let scheduled = false;
260
+ const scheduleFlush = () => {
261
+ if (scheduled) return;
262
+ scheduled = true;
263
+ requestAnimationFrame(() => {
264
+ scheduled = false;
265
+ pending.forEach((n) => {
266
+ try { maybeFix(n); } catch (e) {}
267
+ });
268
+ pending.clear();
269
+ });
270
+ };
253
271
 
254
272
  const obs = new MutationObserver((muts) => {
255
273
  for (const m of muts) {
256
- const t = m.target;
257
274
  if (m.type === 'attributes') {
258
- maybeFix(t && t.nodeType === 1 ? t : t && t.parentElement);
275
+ const t = m.target && m.target.nodeType === 1 ? m.target : m.target && m.target.parentElement;
276
+ if (t) pending.add(t);
259
277
  } else if (m.addedNodes && m.addedNodes.length) {
260
278
  m.addedNodes.forEach((n) => {
261
- if (n && n.nodeType === 1) maybeFix(n);
279
+ if (n && n.nodeType === 1) pending.add(n);
262
280
  });
263
281
  }
264
282
  }
283
+ if (pending.size) scheduleFlush();
265
284
  });
266
- obs.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
285
+ obs.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ['style'] });
267
286
  } catch (e) {}
268
287
  }
269
288
 
@@ -570,12 +589,11 @@ function globalGapFixInit() {
570
589
  if (!state.allPosts.length) state.allPosts = parsePool(cfg.messagePlaceholderIds);
571
590
  if (!state.allCategories.length) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
572
591
 
573
- // Create placeholders up-front in an offscreen pool.
574
- // Ezoic may attempt to define/show ids during load; if they don't exist yet,
575
- // it can spam errors and sometimes short-circuit. Pooling keeps ids existing without layout.
576
- primePlaceholderPool(state.allTopics);
577
- primePlaceholderPool(state.allPosts);
578
- primePlaceholderPool(state.allCategories);
592
+ // IMPORTANT:
593
+ // We do NOT prime a DOM pool anymore.
594
+ // Keeping placeholders connected (even offscreen) can lead Ezoic/GPT to
595
+ // pre-define slots, which then causes "Placeholder Id X has already been defined".
596
+ // Instead, we create the placeholder element only when we actually inject its wrapper.
579
597
  }
580
598
 
581
599
  // ---------------- insertion primitives ----------------
@@ -892,6 +910,16 @@ function globalGapFixInit() {
892
910
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
893
911
  if (!ph || !ph.isConnected) return;
894
912
 
913
+ // If the placeholder already has creative, avoid re-showing.
914
+ // Re-showing is a common source of "Placeholder Id X has already been defined".
915
+ try {
916
+ if (ph.querySelector && ph.querySelector('iframe, ins, img, video, [data-google-container-id]')) {
917
+ clearTimeout(hardTimer);
918
+ release();
919
+ return;
920
+ }
921
+ } catch (e) {}
922
+
895
923
  const t = now();
896
924
  const last = state.lastShowById.get(id) || 0;
897
925
  if (t - last < 900) return;
@@ -1229,13 +1257,29 @@ function globalGapFixInit() {
1229
1257
  requestBurst();
1230
1258
  });
1231
1259
 
1260
+ // Some setups populate content in multiple phases; ensure we re-scan.
1261
+ $(window).on('action:ajaxify.contentLoaded.ezoicInfinite', () => {
1262
+ if (isBlocked()) return;
1263
+ requestBurst();
1264
+ });
1265
+
1232
1266
  $(window).on(
1233
- 'action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite',
1267
+ 'action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:categories.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite',
1234
1268
  () => {
1235
1269
  if (isBlocked()) return;
1236
1270
  requestBurst();
1237
1271
  }
1238
1272
  );
1273
+
1274
+ // Also listen through NodeBB's AMD hooks module when available.
1275
+ try {
1276
+ require(['hooks'], (hooks) => {
1277
+ if (!hooks || typeof hooks.on !== 'function') return;
1278
+ ['action:ajaxify.end', 'action:ajaxify.contentLoaded', 'action:posts.loaded', 'action:topics.loaded', 'action:categories.loaded', 'action:category.loaded', 'action:topic.loaded'].forEach((ev) => {
1279
+ try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (e) {}
1280
+ });
1281
+ });
1282
+ } catch (e) {}
1239
1283
  }
1240
1284
 
1241
1285
  function bindScroll() {