nodebb-plugin-ezoic-infinite 1.8.43 → 1.8.45

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 +52 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.43",
3
+ "version": "1.8.45",
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,5 +1,5 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v3.0.0
2
+ * NodeBB Ezoic Infinite Ads — client.js v3.0.1
3
3
  *
4
4
  * Ezoic API usage per official docs:
5
5
  * https://docs.ezoic.com/docs/ezoicads/dynamic-content/
@@ -33,7 +33,9 @@
33
33
  SHOW_THROTTLE_MS: 900,
34
34
  BURST_COOLDOWN_MS: 200,
35
35
  BLOCK_DURATION_MS: 1_500,
36
- BATCH_FLUSH_MS: 120,
36
+ // Batch Ezoic API calls a bit more aggressively on SPA transitions.
37
+ // This reduces early collector calls (e.g. samo.go) during ajaxify route swaps.
38
+ BATCH_FLUSH_MS: 500,
37
39
  RECYCLE_DESTROY_MS: 300,
38
40
  RECYCLE_SHOW_MS: 300,
39
41
  };
@@ -63,7 +65,8 @@
63
65
 
64
66
  const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
65
67
 
66
- const RECYCLE_MIN_AGE_MS = 5_000;
68
+ // Reduce "ad popping" from aggressive ID recycling.
69
+ const RECYCLE_MIN_AGE_MS = 20_000;
67
70
 
68
71
  // ── Utility ────────────────────────────────────────────────────────────────
69
72
 
@@ -346,16 +349,27 @@
346
349
 
347
350
  function scheduleEmptyCheck(id) {
348
351
  const showTs = now();
349
- setTimeout(() => {
350
- try {
351
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
352
- const wrap = ph?.closest(`.${WRAP_CLASS}`);
353
- if (!wrap || !ph?.isConnected) return;
354
- if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
355
- if (clearEmptyIfFilled(wrap)) return;
356
- wrap.classList.add('is-empty');
357
- } catch (_) {}
358
- }, TIMING.EMPTY_CHECK_MS);
352
+ // Check at 30s, then again at 60s — very conservative to avoid
353
+ // collapsing slow-loading ads
354
+ for (const delay of [30_000, 60_000]) {
355
+ setTimeout(() => {
356
+ try {
357
+ const ph = document.getElementById(`${PH_PREFIX}${id}`);
358
+ const wrap = ph?.closest(`.${WRAP_CLASS}`);
359
+ if (!wrap || !ph?.isConnected) return;
360
+ // Don't collapse if a newer show happened
361
+ if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
362
+ // If already collapsed or uncollapsed, skip
363
+ if (clearEmptyIfFilled(wrap)) return;
364
+ // Don't collapse if there's any GPT slot, even unfilled
365
+ // (GPT may still be processing the ad request)
366
+ if (ph.querySelector('[id^="div-gpt-ad"]')) return;
367
+ // Don't collapse if there's any child with meaningful height
368
+ if (ph.offsetHeight > 10) return;
369
+ wrap.classList.add('is-empty');
370
+ } catch (_) {}
371
+ }, delay);
372
+ }
359
373
  }
360
374
 
361
375
  // ── Recycling ──────────────────────────────────────────────────────────────
@@ -370,7 +384,8 @@
370
384
  typeof ez?.showAds !== 'function') return null;
371
385
 
372
386
  const vh = window.innerHeight || 800;
373
- const threshold = -(3 * vh);
387
+ // Recycle only when the wrap is well above the viewport to avoid visible jumps.
388
+ const threshold = -(6 * vh);
374
389
  const t = now();
375
390
  let bestEmpty = null, bestEmptyY = Infinity;
376
391
  let bestFull = null, bestFullY = Infinity;
@@ -677,7 +692,10 @@
677
692
  step();
678
693
  }
679
694
 
680
- // ── Cleanup ────────────────────────────────────────────────────────────────
695
+ // ── Cleanup on navigation ────────────────────────────────────────────────
696
+ //
697
+ // Only runs on actual page transitions (pageKey changes).
698
+ // Uses destroyAll() to properly clean up Ezoic state before removing DOM.
681
699
 
682
700
  function cleanup() {
683
701
  state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
@@ -686,6 +704,14 @@
686
704
  if (state.ezFlushTimer) { clearTimeout(state.ezFlushTimer); state.ezFlushTimer = null; }
687
705
  state.ezBatch.clear();
688
706
 
707
+ // Tell Ezoic to destroy all its placeholders BEFORE we remove DOM
708
+ try {
709
+ const ez = window.ezstandalone;
710
+ if (typeof ez?.destroyAll === 'function') {
711
+ ez.destroyAll();
712
+ }
713
+ } catch (_) {}
714
+
689
715
  mutate(() => {
690
716
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
691
717
  });
@@ -882,7 +908,17 @@
882
908
  const $ = window.jQuery;
883
909
  if (!$) return;
884
910
  $(window).off('.nbbEzoic');
885
- $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
911
+ $(window).on('action:ajaxify.start.nbbEzoic', (ev, data) => {
912
+ // Only cleanup if navigating to a different page
913
+ // NodeBB fires ajaxify.start for pagination/sorting on the same page
914
+ const targetUrl = data?.url || data?.tpl_url || '';
915
+ const currentPath = location.pathname.replace(/^\//, '');
916
+ // If the URL is basically the same (ignoring query/hash), skip cleanup
917
+ if (targetUrl && targetUrl.replace(/[?#].*$/, '') === currentPath.replace(/[?#].*$/, '')) {
918
+ return;
919
+ }
920
+ cleanup();
921
+ });
886
922
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
887
923
  state.pageKey = pageKey();
888
924
  state.kind = null;