nodebb-plugin-ezoic-infinite 1.5.50 → 1.5.52

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.50",
3
+ "version": "1.5.52",
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
@@ -96,48 +96,6 @@
96
96
 
97
97
  const insertingIds = new Set();
98
98
 
99
- // ---------- lightweight "fade-in" for ad iframes ----------
100
- // This reduces the perception of "flashing" when a slot appears empty then fills.
101
- // We avoid scroll listeners and only react to DOM insertions.
102
- const _faded = new WeakSet();
103
- function _fadeInIframe(iframe) {
104
- try {
105
- if (!iframe || _faded.has(iframe)) return;
106
- _faded.add(iframe);
107
- iframe.style.opacity = '0';
108
- iframe.style.transition = 'opacity 140ms ease';
109
- // Next frame: show
110
- requestAnimationFrame(() => {
111
- try { iframe.style.opacity = '1'; } catch (e) {}
112
- });
113
- } catch (e) {}
114
- }
115
-
116
- const _adFillObserver = new MutationObserver((muts) => {
117
- try {
118
- for (const m of muts) {
119
- if (m.addedNodes && m.addedNodes.length) {
120
- for (const n of m.addedNodes) {
121
- if (!n || n.nodeType !== 1) continue;
122
- if (n.tagName === 'IFRAME') {
123
- const w = n.closest && n.closest(`.${WRAP_CLASS}`);
124
- if (w) _fadeInIframe(n);
125
- continue;
126
- }
127
- // If a subtree is added, look for iframes inside ad wrappers only.
128
- const ifs = n.querySelectorAll ? n.querySelectorAll(`.${WRAP_CLASS} iframe`) : null;
129
- if (ifs && ifs.length) ifs.forEach(_fadeInIframe);
130
- }
131
- }
132
- }
133
- } catch (e) {}
134
- });
135
-
136
- try {
137
- _adFillObserver.observe(document.documentElement, { subtree: true, childList: true });
138
- } catch (e) {}
139
-
140
-
141
99
 
142
100
  function markEmptyWrapper(id) {
143
101
  try {
@@ -247,18 +205,6 @@
247
205
  ['dns-prefetch', 'https://g.ezoic.net', false],
248
206
  ['preconnect', 'https://go.ezoic.net', true],
249
207
  ['dns-prefetch', 'https://go.ezoic.net', false],
250
-
251
- // Google ad stack (helps Safeframe/GPT warm up)
252
- ['preconnect', 'https://pagead2.googlesyndication.com', true],
253
- ['dns-prefetch', 'https://pagead2.googlesyndication.com', false],
254
- ['preconnect', 'https://securepubads.g.doubleclick.net', true],
255
- ['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
256
- ['preconnect', 'https://tpc.googlesyndication.com', true],
257
- ['dns-prefetch', 'https://tpc.googlesyndication.com', false],
258
- ['preconnect', 'https://googleads.g.doubleclick.net', true],
259
- ['dns-prefetch', 'https://googleads.g.doubleclick.net', false],
260
- ['preconnect', 'https://static.doubleclick.net', true],
261
- ['dns-prefetch', 'https://static.doubleclick.net', false],
262
208
  ];
263
209
  for (const [rel, href, cors] of links) {
264
210
  const key = `${rel}|${href}`;
@@ -274,46 +220,76 @@
274
220
  }
275
221
 
276
222
  // Patch showAds to avoid warnings when a placeholder disappears (infinite scroll, ajaxify)
277
- function patchShowAds() {
278
- const applyPatch = () => {
279
- try {
280
- window.ezstandalone = window.ezstandalone || {};
281
- const ez = window.ezstandalone;
282
- if (window.__nodebbEzoicPatched) return;
283
- if (typeof ez.showAds !== 'function') return;
284
-
285
- window.__nodebbEzoicPatched = true;
286
- const orig = ez.showAds;
287
-
288
- ez.showAds = function (...args) {
289
- if (isBlocked()) return;
290
-
291
- let ids = [];
292
- if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
293
- else ids = args;
294
-
295
- const seen = new Set();
296
- for (const v of ids) {
297
- const id = parseInt(v, 10);
298
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
299
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
300
- if (!ph || !ph.isConnected) continue;
301
- seen.add(id);
302
- try { orig.call(ez, id); } catch (e) {}
303
- }
304
- };
305
- } catch (e) {}
223
+
224
+ function patchShowAds() {
225
+ // Robustly filter ez.showAds calls so Ezoic won't spam "placeholder does not exist"
226
+ // during ajaxify navigation or when some placeholders are not present on the current view.
227
+ try {
228
+ window.ezstandalone = window.ezstandalone || {};
229
+ const ez = window.ezstandalone;
230
+ ez.cmd = ez.cmd || [];
231
+
232
+ const wrap = (orig) => {
233
+ if (typeof orig !== 'function') return orig;
234
+ if (orig.__nodebbWrapped) return orig;
235
+
236
+ const wrapped = function (...args) {
237
+ if (isBlocked()) return;
238
+
239
+ let ids = [];
240
+ if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
241
+ else ids = args;
242
+
243
+ const filtered = [];
244
+ const seen = new Set();
245
+ for (const v of ids) {
246
+ const id = parseInt(v, 10);
247
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
248
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
249
+ if (!ph || !ph.isConnected) continue;
250
+ seen.add(id);
251
+ filtered.push(id);
252
+ }
253
+ if (!filtered.length) return;
254
+
255
+ // Call original with the same shape most implementations accept
256
+ try { return orig.call(ez, filtered.length === 1 ? filtered[0] : filtered); } catch (e) {}
257
+ };
258
+ wrapped.__nodebbWrapped = true;
259
+ wrapped.__nodebbOrig = orig;
260
+ return wrapped;
306
261
  };
307
262
 
308
- applyPatch();
309
- if (!window.__nodebbEzoicPatched) {
310
- try {
311
- window.ezstandalone = window.ezstandalone || {};
312
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
313
- window.ezstandalone.cmd.push(applyPatch);
314
- } catch (e) {}
263
+ // If showAds already exists, wrap it now.
264
+ if (typeof ez.showAds === 'function') {
265
+ ez.showAds = wrap(ez.showAds);
266
+ return;
315
267
  }
316
- }
268
+
269
+ // Otherwise, define a setter hook so whenever Ezoic defines showAds, we wrap it.
270
+ if (!ez.__nodebbShowAdsHooked) {
271
+ ez.__nodebbShowAdsHooked = true;
272
+ let _showAds = null;
273
+
274
+ Object.defineProperty(ez, 'showAds', {
275
+ configurable: true,
276
+ enumerable: true,
277
+ get() { return _showAds; },
278
+ set(fn) { _showAds = wrap(fn); }
279
+ });
280
+
281
+ // In case Ezoic sets showAds from cmd queue
282
+ ez.cmd.push(() => {
283
+ try {
284
+ if (typeof ez.showAds === 'function' && !ez.showAds.__nodebbWrapped) {
285
+ ez.showAds = wrap(ez.showAds);
286
+ }
287
+ } catch (e) {}
288
+ });
289
+ }
290
+ } catch (e) {}
291
+ }
292
+
317
293
 
318
294
  const RECYCLE_COOLDOWN_MS = 1500;
319
295
 
@@ -439,7 +415,6 @@ function buildWrap(id, kindClass, afterPos) {
439
415
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
440
416
  wrap.setAttribute('data-ezoic-after', String(afterPos));
441
417
  wrap.setAttribute('data-ezoic-wrapid', String(id));
442
- wrap.setAttribute('data-ezoic-ts', String(Date.now()));
443
418
  wrap.style.width = '100%';
444
419
 
445
420
  const ph = document.createElement('div');
@@ -496,23 +471,14 @@ function buildWrap(id, kindClass, afterPos) {
496
471
  const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
497
472
  if (!wraps.length) return false;
498
473
 
499
- const now = Date.now();
500
-
501
- // Only recycle wraps that are clearly out of view AND old enough.
502
- // This avoids the "unstable" feeling where ads disappear when the user scrolls back a bit.
503
- const MIN_AGE_MS = 20000; // 20s
504
- const OFFSCREEN_PX = -5000; // far above viewport
505
-
474
+ // Prefer a wrap far above the viewport
506
475
  let victim = null;
507
476
  for (const w of wraps) {
508
477
  const r = w.getBoundingClientRect();
509
- const ts = parseInt(w.getAttribute('data-ezoic-ts') || '0', 10);
510
- const ageOk = !ts || (now - ts) >= MIN_AGE_MS;
511
- if (ageOk && r.bottom < OFFSCREEN_PX) { victim = w; break; }
478
+ if (r.bottom < -2000) { victim = w; break; }
512
479
  }
513
-
514
- // If nothing is eligible, do not recycle. We'll simply skip inserting new ads this run.
515
- if (!victim) return false;
480
+ // Otherwise remove the earliest one in the document
481
+ if (!victim) victim = wraps[0];
516
482
 
517
483
  // Unobserve placeholder if still observed
518
484
  try {
@@ -559,6 +525,7 @@ function drainQueue() {
559
525
  function startShow(id) {
560
526
  if (!id || isBlocked()) return;
561
527
 
528
+ // Reserve one inflight slot for this request
562
529
  state.inflight++;
563
530
  let released = false;
564
531
  const release = () => {
@@ -570,16 +537,22 @@ function startShow(id) {
570
537
 
571
538
  const hardTimer = setTimeout(release, 6500);
572
539
 
540
+ const earlyExit = () => {
541
+ try { clearTimeout(hardTimer); } catch (e) {}
542
+ release();
543
+ };
544
+
573
545
  requestAnimationFrame(() => {
574
546
  try {
575
- if (isBlocked()) return;
547
+ if (isBlocked()) return earlyExit();
576
548
 
577
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
578
- if (!ph || !ph.isConnected) return;
549
+ const domId = `${PLACEHOLDER_PREFIX}${id}`;
550
+ const ph = document.getElementById(domId);
551
+ if (!ph || !ph.isConnected) return earlyExit();
579
552
 
580
553
  const now2 = Date.now();
581
554
  const last2 = state.lastShowById.get(id) || 0;
582
- if (now2 - last2 < 900) return;
555
+ if (now2 - last2 < 900) return earlyExit();
583
556
  state.lastShowById.set(id, now2);
584
557
 
585
558
  window.ezstandalone = window.ezstandalone || {};
@@ -587,16 +560,31 @@ function startShow(id) {
587
560
 
588
561
  const doShow = () => {
589
562
  try {
563
+ if (isBlocked()) return earlyExit();
564
+
565
+ // Re-check at execution time (cmd queue may delay)
566
+ const ph2 = document.getElementById(domId);
567
+ if (!ph2 || !ph2.isConnected) return earlyExit();
568
+
569
+ // If this id was used before in this pageview, destroy first (Ezoic best practice on recycle)
590
570
  if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
591
- try { ez.destroyPlaceholders(id); } catch (e) {}
571
+ try { ez.destroyPlaceholders([domId]); } catch (e) {}
592
572
  }
593
- } catch (e) {}
594
573
 
595
- try { ez.showAds(id); } catch (e) {}
596
- try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
597
- try { markEmptyWrapper(id); } catch (e) {}
574
+ // Call showAds (patched to filter missing placeholders)
575
+ try { ez.showAds(id); } catch (e) {}
576
+
577
+ try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
578
+ try { markEmptyWrapper(id); } catch (e) {} // harmless; if removed later, it no-ops
598
579
 
599
- setTimeout(() => { clearTimeout(hardTimer); release(); }, 650);
580
+ // Release shortly after triggering, so we can pipeline the next ones
581
+ setTimeout(() => {
582
+ try { clearTimeout(hardTimer); } catch (e) {}
583
+ release();
584
+ }, 650);
585
+ } catch (e) {
586
+ earlyExit();
587
+ }
600
588
  };
601
589
 
602
590
  if (Array.isArray(ez.cmd)) {
@@ -604,8 +592,8 @@ function startShow(id) {
604
592
  } else {
605
593
  doShow();
606
594
  }
607
- } finally {
608
- // If we returned early, hardTimer will release.
595
+ } catch (e) {
596
+ earlyExit();
609
597
  }
610
598
  });
611
599
  }
package/public/style.css CHANGED
@@ -38,9 +38,3 @@
38
38
  .ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
39
39
  min-height: 0 !important;
40
40
  }
41
-
42
- /* Avoid baseline gap under iframes (can look like extra space) */
43
- .ezoic-ad iframe {
44
- display: block !important;
45
- vertical-align: top !important;
46
- }