nodebb-plugin-ezoic-infinite 1.5.26 → 1.5.27

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 +102 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.26",
3
+ "version": "1.5.27",
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
@@ -19,8 +19,11 @@
19
19
  categoryItem: 'li[component="categories/category"]',
20
20
  };
21
21
 
22
- // Hard block during navigation to avoid “placeholder does not exist” spam
23
- let EZOIC_BLOCKED = false;
22
+ // Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
23
+ let blockedUntil = 0;
24
+ function isBlocked() {
25
+ return Date.now() < blockedUntil;
26
+ }
24
27
 
25
28
  const state = {
26
29
  pageKey: null,
@@ -36,6 +39,8 @@
36
39
 
37
40
  // throttle per placeholder id
38
41
  lastShowById: new Map(),
42
+ internalDomChange: 0,
43
+ lastRecycleAt: { topic: 0, categoryTopics: 0, categories: 0 },
39
44
 
40
45
  // track placeholders that have been shown at least once in this pageview
41
46
  usedOnce: new Set(),
@@ -172,7 +177,7 @@
172
177
  const orig = ez.showAds;
173
178
 
174
179
  ez.showAds = function (...args) {
175
- if (EZOIC_BLOCKED) return;
180
+ if (isBlocked()) return;
176
181
 
177
182
  let ids = [];
178
183
  if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
@@ -201,7 +206,28 @@
201
206
  }
202
207
  }
203
208
 
204
- // ---------- config & pools ----------
209
+ const RECYCLE_COOLDOWN_MS = 1500;
210
+
211
+ function kindKeyFromClass(kindClass) {
212
+ if (kindClass === 'ezoic-ad-message') return 'topic';
213
+ if (kindClass === 'ezoic-ad-between') return 'categoryTopics';
214
+ if (kindClass === 'ezoic-ad-categories') return 'categories';
215
+ return 'topic';
216
+ }
217
+
218
+ function withInternalDomChange(fn) {
219
+ state.internalDomChange++;
220
+ try { fn(); } finally { state.internalDomChange--; }
221
+ }
222
+
223
+ function canRecycle(kind) {
224
+ const now = Date.now();
225
+ const last = state.lastRecycleAt[kind] || 0;
226
+ if (now - last < RECYCLE_COOLDOWN_MS) return false;
227
+ state.lastRecycleAt[kind] = now;
228
+ return true;
229
+ }
230
+ // ---------- config & pools ----------
205
231
 
206
232
  async function fetchConfigOnce() {
207
233
  if (state.cfg) return state.cfg;
@@ -316,56 +342,71 @@
316
342
  }
317
343
 
318
344
  function showAd(id) {
319
- if (!id || EZOIC_BLOCKED) return;
345
+ if (!id || isBlocked()) return;
320
346
 
321
347
  const now = Date.now();
322
348
  const last = state.lastShowById.get(id) || 0;
323
349
  if (now - last < 1500) return; // basic throttle
324
350
 
325
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
326
- if (!ph || !ph.isConnected) return;
351
+ // Defer one frame so the placeholder is definitely in DOM after insertion/recycle
352
+ requestAnimationFrame(() => {
353
+ if (isBlocked()) return;
327
354
 
328
- state.lastShowById.set(id, now);
355
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
356
+ if (!ph || !ph.isConnected) return;
329
357
 
330
- try {
331
- window.ezstandalone = window.ezstandalone || {};
332
- const ez = window.ezstandalone;
333
-
334
- // Fast path
335
- if (typeof ez.showAds === 'function') {
336
- try {
337
- if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
338
- ez.destroyPlaceholders(id);
339
- }
340
- } catch (e) {}
341
- ez.showAds(id);
342
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
343
- return;
344
- }
358
+ const now2 = Date.now();
359
+ const last2 = state.lastShowById.get(id) || 0;
360
+ if (now2 - last2 < 1200) return;
361
+ state.lastShowById.set(id, now2);
345
362
 
346
- // Queue once for when Ezoic is ready
347
- ez.cmd = ez.cmd || [];
348
- if (!ph.__ezoicQueued) {
349
- ph.__ezoicQueued = true;
350
- ez.cmd.push(() => {
363
+ try {
364
+ window.ezstandalone = window.ezstandalone || {};
365
+ const ez = window.ezstandalone;
366
+
367
+ const doShow = () => {
351
368
  try {
352
- if (EZOIC_BLOCKED) return;
353
- const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
354
- if (!el || !el.isConnected) return;
355
- const ez2 = window.ezstandalone;
369
+ if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
370
+ // Avoid Ezoic caching state for reused placeholders
371
+ ez.destroyPlaceholders(id);
372
+ }
373
+ } catch (e) {}
374
+ try { ez.showAds(id); } catch (e) {}
375
+ try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
376
+ };
377
+
378
+ // Fast path
379
+ if (typeof ez.showAds === 'function') {
380
+ doShow();
381
+ return;
382
+ }
383
+
384
+ // Queue once for when Ezoic is ready
385
+ ez.cmd = ez.cmd || [];
386
+ if (!ph.__ezoicQueued) {
387
+ ph.__ezoicQueued = true;
388
+ ez.cmd.push(() => {
356
389
  try {
357
- if (state.usedOnce && state.usedOnce.has(id) && typeof ez2.destroyPlaceholders === 'function') {
358
- ez2.destroyPlaceholders(id);
359
- }
390
+ if (isBlocked()) return;
391
+ const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
392
+ if (!el || !el.isConnected) return;
393
+ const ez2 = window.ezstandalone;
394
+ if (!ez2 || typeof ez2.showAds !== 'function') return;
395
+ try {
396
+ if (state.usedOnce && state.usedOnce.has(id) && typeof ez2.destroyPlaceholders === 'function') {
397
+ ez2.destroyPlaceholders(id);
398
+ }
399
+ } catch (e) {}
400
+ try { ez2.showAds(id); } catch (e) {}
401
+ try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
360
402
  } catch (e) {}
361
- ez2.showAds(id);
362
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
363
- } catch (e) {}
364
- });
365
- }
366
- } catch (e) {}
403
+ });
404
+ }
405
+ } catch (e) {}
406
+ });
367
407
  }
368
408
 
409
+
369
410
  // ---------- preload / above-the-fold ----------
370
411
 
371
412
  function ensurePreloadObserver() {
@@ -429,12 +470,20 @@
429
470
 
430
471
  let id = pickIdFromAll(allIds, cursorKey);
431
472
  if (!id) {
432
- // No free ids: recycle an old ad wrapper so we can reuse its placeholder id
433
- const recycled = removeOneOldWrap(kindClass);
473
+ // No free ids: recycle an old ad wrapper so we can reuse its placeholder id.
474
+ // Guard against tight observer loops.
475
+ if (!canRecycle(kindKeyFromClass(kindClass))) {
476
+ dbg('recycle-skip-cooldown', kindClass);
477
+ break;
478
+ }
479
+ let recycled = false;
480
+ withInternalDomChange(() => {
481
+ recycled = removeOneOldWrap(kindClass);
482
+ });
434
483
  dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
435
- id = pickIdFromAll(allIds, cursorKey);
484
+ // Stop this run after a recycle; the next mutation/scroll will retry injection.
485
+ break;
436
486
  }
437
- if (!id) break;
438
487
  const wrap = insertAfter(el, id, kindClass, afterPos);
439
488
  if (!wrap) {
440
489
  continue;
@@ -507,7 +556,7 @@
507
556
  }
508
557
 
509
558
  async function runCore() {
510
- if (EZOIC_BLOCKED) { dbg('blocked'); return; }
559
+ if (isBlocked()) { dbg('blocked'); return; }
511
560
 
512
561
  patchShowAds();
513
562
 
@@ -568,7 +617,7 @@
568
617
  // ---------- observers / lifecycle ----------
569
618
 
570
619
  function cleanup() {
571
- EZOIC_BLOCKED = true;
620
+ blockedUntil = Date.now() + 1200;
572
621
 
573
622
  // remove all wrappers
574
623
  try {
@@ -595,7 +644,8 @@
595
644
  function ensureDomObserver() {
596
645
  if (state.domObs) return;
597
646
  state.domObs = new MutationObserver(() => {
598
- if (!EZOIC_BLOCKED) scheduleRun();
647
+ if (state.internalDomChange > 0) return;
648
+ if (!isBlocked()) scheduleRun();
599
649
  });
600
650
  try {
601
651
  state.domObs.observe(document.body, { childList: true, subtree: true });
@@ -613,7 +663,7 @@
613
663
 
614
664
  $(window).on('action:ajaxify.end.ezoicInfinite', () => {
615
665
  state.pageKey = getPageKey();
616
- EZOIC_BLOCKED = false;
666
+ blockedUntil = 0;
617
667
 
618
668
  warmUpNetwork();
619
669
  patchShowAds();
@@ -629,7 +679,7 @@
629
679
 
630
680
  // Infinite scroll / partial updates
631
681
  $(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
632
- if (EZOIC_BLOCKED) return;
682
+ if (isBlocked()) return;
633
683
  scheduleRun();
634
684
  });
635
685
  }
@@ -641,7 +691,7 @@
641
691
  ticking = true;
642
692
  window.requestAnimationFrame(() => {
643
693
  ticking = false;
644
- if (!EZOIC_BLOCKED) scheduleRun();
694
+ if (!isBlocked()) scheduleRun();
645
695
  });
646
696
  }, { passive: true });
647
697
  }
@@ -658,7 +708,7 @@
658
708
  bindScroll();
659
709
 
660
710
  // First paint: try hero + run
661
- EZOIC_BLOCKED = false;
711
+ blockedUntil = 0;
662
712
  insertHeroAdEarly().catch(() => {});
663
713
  scheduleRun();
664
714
  })();