nodebb-plugin-ezoic-infinite 1.5.66 → 1.5.68

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.66",
3
+ "version": "1.5.68",
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
@@ -82,10 +82,6 @@
82
82
  io: null,
83
83
  runQueued: false,
84
84
 
85
- needsMoreRun: false,
86
- moreRunBurst: 0,
87
- moreRunLast: 0,
88
-
89
85
  // preloading budget
90
86
  inflight: 0,
91
87
  pending: [],
@@ -99,19 +95,40 @@
99
95
 
100
96
  // hero)
101
97
  heroDoneForPage: false,
98
+ burstRuns: 0,
102
99
  };
103
100
 
104
101
  const insertingIds = new Set();
105
102
 
106
- function unemptyIfFilled(wrap, ph) {
107
- try {
108
- if (!wrap || !ph) return false;
109
- const hasAd = !!(ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
110
- if (hasAd) wrap.classList.remove('is-empty');
111
- return hasAd;
112
- } catch (e) { return false; }
103
+ // Hidden pool where we keep placeholder DIVs when they are not currently
104
+ // mounted in the content flow. This avoids:
105
+ // - Ezoic trying to act on ids that were removed from the DOM ("does not exist")
106
+ // - re-defining placeholders (we re-use the same node)
107
+ const POOL_ID = 'nodebb-ezoic-placeholder-pool';
108
+ function getPoolEl() {
109
+ let el = document.getElementById(POOL_ID);
110
+ if (el) return el;
111
+ el = document.createElement('div');
112
+ el.id = POOL_ID;
113
+ el.style.cssText = 'position:fixed;left:-99999px;top:-99999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;';
114
+ (document.body || document.documentElement).appendChild(el);
115
+ return el;
116
+ }
117
+
118
+ function isInPool(ph) {
119
+ try { return !!(ph && ph.closest && ph.closest('#' + POOL_ID)); } catch (e) { return false; }
113
120
  }
114
121
 
122
+ function releaseWrapNode(wrap) {
123
+ try {
124
+ const ph = wrap && wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
125
+ if (ph) {
126
+ try { getPoolEl().appendChild(ph); } catch (e) {}
127
+ try { if (state.io) state.io.unobserve(ph); } catch (e) {}
128
+ }
129
+ } catch (e) {}
130
+ try { wrap && wrap.remove && wrap.remove(); } catch (e) {}
131
+ }
115
132
 
116
133
 
117
134
  function markEmptyWrapper(id) {
@@ -130,17 +147,6 @@
130
147
  // consider empty if only whitespace and no iframes/ins/img
131
148
  const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
132
149
  if (!hasAd) w2.classList.add('is-empty');
133
- // If the ad fills later, immediately uncollapse to avoid "missing ads" perception.
134
- if (!unemptyIfFilled(w2, ph2)) {
135
- try {
136
- const mo = new MutationObserver(() => {
137
- if (unemptyIfFilled(w2, ph2)) { try { mo.disconnect(); } catch (e) {} }
138
- });
139
- mo.observe(ph2, { childList: true, subtree: true });
140
- // safety stop
141
- setTimeout(() => { try { mo.disconnect(); } catch (e) {} }, 30000);
142
- } catch (e) {}
143
- }
144
150
  } catch (e) {}
145
151
  }, 3500);
146
152
  } catch (e) {}
@@ -259,40 +265,22 @@
259
265
  window.__nodebbEzoicPatched = true;
260
266
  const orig = ez.showAds;
261
267
 
262
- // Important: preserve the original calling convention.
263
- // Some Ezoic builds expect an array; calling one-by-one can lead to
264
- // repeated define attempts and "Placeholder Id ... already been defined".
265
268
  ez.showAds = function (...args) {
266
269
  if (isBlocked()) return;
267
270
 
268
- const now = Date.now();
269
271
  let ids = [];
270
- const isArrayCall = (args.length === 1 && Array.isArray(args[0]));
271
- if (isArrayCall) ids = args[0];
272
+ if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
272
273
  else ids = args;
273
274
 
274
- const filtered = [];
275
275
  const seen = new Set();
276
276
  for (const v of ids) {
277
277
  const id = parseInt(v, 10);
278
278
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
279
279
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
280
280
  if (!ph || !ph.isConnected) continue;
281
-
282
- // Extra throttle to avoid rapid duplicate defines during ajaxify churn
283
- const last = state.lastShowById.get(id) || 0;
284
- if (now - last < 650) continue;
285
- state.lastShowById.set(id, now);
286
-
287
281
  seen.add(id);
288
- filtered.push(id);
282
+ try { orig.call(ez, id); } catch (e) {}
289
283
  }
290
-
291
- if (!filtered.length) return;
292
- try {
293
- if (isArrayCall) orig.call(ez, filtered);
294
- else orig.apply(ez, filtered);
295
- } catch (e) {}
296
284
  };
297
285
  } catch (e) {}
298
286
  };
@@ -390,27 +378,17 @@ function withInternalDomChange(fn) {
390
378
  // NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
391
379
  let ok = false;
392
380
  let prev = wrap.previousElementSibling;
393
- for (let i = 0; i < 8 && prev; i++) {
381
+ for (let i = 0; i < 3 && prev; i++) {
394
382
  if (itemSet.has(prev)) { ok = true; break; }
395
383
  prev = prev.previousElementSibling;
396
384
  }
397
385
 
398
- // If it is already filled (iframe/ins/img), be conservative and keep it.
399
- // Prevents ads "disappearing too fast" during ajaxify churn / minor rerenders.
400
386
  if (!ok) {
401
- try {
402
- const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
403
- const filled = !!(ph && ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
404
- if (filled) ok = true;
405
- } catch (e) {}
406
- }
407
-
408
- if (!ok) {
409
- const id = getWrapIdFromWrap(wrap);
410
387
  withInternalDomChange(() => {
411
388
  try {
412
- if (id) safeDestroyById(id);
413
- wrap.remove();
389
+ // Do not destroy placeholders; move them back to the pool so
390
+ // Ezoic won't log "does not exist" and we can reuse them later.
391
+ releaseWrapNode(wrap);
414
392
  } catch (e) {}
415
393
  });
416
394
  removed++;
@@ -421,28 +399,6 @@ function withInternalDomChange(fn) {
421
399
  return removed;
422
400
  }
423
401
 
424
- function declusterWraps(kindClass) {
425
- try {
426
- const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
427
- if (wraps.length < 2) return;
428
- for (let i = 1; i < wraps.length; i++) {
429
- const w = wraps[i];
430
- if (!w || !w.isConnected) continue;
431
- // If previous siblings contain another wrap within 2 hops, remove this one.
432
- let prev = w.previousElementSibling;
433
- let hops = 0;
434
- while (prev && hops < 3) {
435
- if (prev.classList && prev.classList.contains(WRAP_CLASS)) {
436
- withInternalDomChange(() => { try { w.remove(); } catch (e) {} });
437
- break;
438
- }
439
- prev = prev.previousElementSibling;
440
- hops++;
441
- }
442
- }
443
- } catch (e) {}
444
- }
445
-
446
402
  function refreshEmptyState(id) {
447
403
  // After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
448
404
  window.setTimeout(() => {
@@ -458,20 +414,14 @@ function withInternalDomChange(fn) {
458
414
  }, 3500);
459
415
  }
460
416
 
461
- function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
417
+ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
462
418
  const wrap = document.createElement('div');
463
419
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
464
420
  wrap.setAttribute('data-ezoic-after', String(afterPos));
465
421
  wrap.setAttribute('data-ezoic-wrapid', String(id));
466
422
  wrap.style.width = '100%';
467
423
 
468
- if (existingPlaceholder && existingPlaceholder.nodeType === 1) {
469
- try {
470
- existingPlaceholder.id = `${PLACEHOLDER_PREFIX}${id}`;
471
- existingPlaceholder.setAttribute('data-ezoic-id', String(id));
472
- } catch (e) {}
473
- wrap.appendChild(existingPlaceholder);
474
- } else {
424
+ if (createPlaceholder) {
475
425
  const ph = document.createElement('div');
476
426
  ph.id = `${PLACEHOLDER_PREFIX}${id}`;
477
427
  ph.setAttribute('data-ezoic-id', String(id));
@@ -490,28 +440,28 @@ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
490
440
  if (findWrap(kindClass, afterPos)) return null;
491
441
  if (insertingIds.has(id)) return null;
492
442
 
443
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
444
+
493
445
  insertingIds.add(id);
494
446
  try {
495
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
496
-
497
- // CRITICAL: never create a second element with the same id, even briefly.
498
- // That can trigger "Placeholder Id ... already been defined" during load.
499
- // If an existing placeholder already exists, move it into the new wrapper
500
- // before inserting the wrapper into the DOM.
501
- let moved = null;
502
- if (existingPh && existingPh.isConnected) {
503
- moved = existingPh;
504
- // If it was inside one of our wrappers, drop that empty wrapper.
447
+ // If a placeholder already exists (either in content or in our pool),
448
+ // do NOT create a new DOM node with the same id even temporarily.
449
+ const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
450
+ target.insertAdjacentElement('afterend', wrap);
451
+
452
+ // If a placeholder with this id already exists elsewhere (some Ezoic flows
453
+ // pre-create placeholders), move it into our wrapper instead of aborting.
454
+ // replaceChild moves the node atomically (no detach window).
455
+ if (existingPh) {
505
456
  try {
506
- const oldWrap = moved.closest && moved.closest(`.${WRAP_CLASS}`);
507
- if (oldWrap && oldWrap.parentNode) {
508
- withInternalDomChange(() => { try { oldWrap.remove(); } catch (e) {} });
509
- }
510
- } catch (e) {}
457
+ existingPh.setAttribute('data-ezoic-id', String(id));
458
+ // If we didn't create a placeholder, just append.
459
+ if (!wrap.firstElementChild) wrap.appendChild(existingPh);
460
+ else wrap.replaceChild(existingPh, wrap.firstElementChild);
461
+ } catch (e) {
462
+ // Keep the new placeholder if replace fails.
463
+ }
511
464
  }
512
-
513
- const wrap = buildWrap(id, kindClass, afterPos, moved);
514
- target.insertAdjacentElement('afterend', wrap);
515
465
  return wrap;
516
466
  } finally {
517
467
  insertingIds.delete(id);
@@ -529,7 +479,9 @@ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
529
479
 
530
480
  const id = allIds[idx];
531
481
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
532
- if (ph && ph.isConnected) continue;
482
+ // If placeholder is currently mounted in the content flow, skip.
483
+ // If it's in our hidden pool, it's available for reuse.
484
+ if (ph && ph.isConnected && !isInPool(ph)) continue;
533
485
 
534
486
  return id;
535
487
  }
@@ -551,13 +503,7 @@ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
551
503
  // Otherwise remove the earliest one in the document
552
504
  if (!victim) victim = wraps[0];
553
505
 
554
- // Unobserve placeholder if still observed
555
- try {
556
- const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
557
- if (ph && state.io) state.io.unobserve(ph);
558
- } catch (e) {}
559
-
560
- victim.remove();
506
+ releaseWrapNode(victim);
561
507
  return true;
562
508
  } catch (e) {
563
509
  return false;
@@ -720,10 +666,8 @@ function startShow(id) {
720
666
  let inserted = 0;
721
667
  const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
722
668
 
723
- let hitLimit = false;
724
-
725
669
  for (const afterPos of targets) {
726
- if (inserted >= maxInserts) { hitLimit = true; break; }
670
+ if (inserted >= maxInserts) break;
727
671
 
728
672
  const el = items[afterPos - 1];
729
673
  if (!el || !el.isConnected) continue;
@@ -755,11 +699,6 @@ function startShow(id) {
755
699
  inserted += 1;
756
700
  }
757
701
 
758
- if (hitLimit) {
759
- // We intentionally cap per run for smoothness. If we hit the cap, queue another pass.
760
- state.needsMoreRun = true;
761
- }
762
-
763
702
  return inserted;
764
703
  }
765
704
 
@@ -771,8 +710,6 @@ function startShow(id) {
771
710
 
772
711
  initPools(cfg);
773
712
 
774
- state.needsMoreRun = false;
775
-
776
713
  const kind = getKind();
777
714
  let items = [];
778
715
  let allIds = [];
@@ -826,6 +763,7 @@ function startShow(id) {
826
763
 
827
764
  async function runCore() {
828
765
  if (isBlocked()) { dbg('blocked'); return; }
766
+ let insertedThisRun = 0;
829
767
 
830
768
  patchShowAds();
831
769
 
@@ -840,7 +778,7 @@ function startShow(id) {
840
778
  if (normalizeBool(cfg.enableMessageAds)) {
841
779
  const __items = getPostContainers();
842
780
  pruneOrphanWraps('ezoic-ad-message', __items);
843
- injectBetween(
781
+ insertedThisRun += injectBetween(
844
782
  'ezoic-ad-message',
845
783
  __items,
846
784
  Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
@@ -848,13 +786,12 @@ function startShow(id) {
848
786
  state.allPosts,
849
787
  'curPosts'
850
788
  );
851
- declusterWraps('ezoic-ad-message');
852
789
  }
853
790
  } else if (kind === 'categoryTopics') {
854
791
  if (normalizeBool(cfg.enableBetweenAds)) {
855
792
  const __items = getTopicItems();
856
793
  pruneOrphanWraps('ezoic-ad-between', __items);
857
- injectBetween(
794
+ insertedThisRun += injectBetween(
858
795
  'ezoic-ad-between',
859
796
  __items,
860
797
  Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
@@ -862,13 +799,12 @@ function startShow(id) {
862
799
  state.allTopics,
863
800
  'curTopics'
864
801
  );
865
- declusterWraps('ezoic-ad-between');
866
802
  }
867
803
  } else if (kind === 'categories') {
868
804
  if (normalizeBool(cfg.enableCategoryAds)) {
869
805
  const __items = getCategoryItems();
870
806
  pruneOrphanWraps('ezoic-ad-categories', __items);
871
- injectBetween(
807
+ insertedThisRun += injectBetween(
872
808
  'ezoic-ad-categories',
873
809
  __items,
874
810
  Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
@@ -876,34 +812,47 @@ function startShow(id) {
876
812
  state.allCategories,
877
813
  'curCategories'
878
814
  );
879
- declusterWraps('ezoic-ad-categories');
880
815
  }
881
816
  }
882
- // If we hit the per-run insertion cap, schedule another pass soon.
883
- // Guard against infinite loops when nothing more can be inserted (e.g. no ids / adjacency everywhere).
884
- if (state.needsMoreRun) {
885
- const now = Date.now();
886
- if (now - state.moreRunLast > 800) state.moreRunBurst = 0;
887
- state.moreRunLast = now;
888
- state.moreRunBurst += 1;
889
- if (state.moreRunBurst <= 8) {
890
- setTimeout(scheduleRun, 60);
891
- }
892
- }
893
-
894
817
  }
895
818
 
896
- function scheduleRun() {
819
+ function scheduleRun(delayMs = 0) {
820
+ // schedule a single run (coalesced)
897
821
  if (state.runQueued) return;
898
822
  state.runQueued = true;
899
- window.requestAnimationFrame(() => {
823
+ const doRun = () => {
900
824
  state.runQueued = false;
901
825
  const pk = getPageKey();
902
826
  if (state.pageKey && pk !== state.pageKey) return;
903
827
  runCore().catch(() => {});
904
- });
828
+ };
829
+ if (delayMs > 0) {
830
+ window.setTimeout(() => window.requestAnimationFrame(doRun), delayMs);
831
+ } else {
832
+ window.requestAnimationFrame(doRun);
833
+ }
905
834
  }
906
835
 
836
+ function scheduleBurst() {
837
+ // During ajaxify/infinite scroll, the DOM may arrive in waves.
838
+ // We run a small, bounded burst to ensure all 1/X targets are reached.
839
+ const pk = getPageKey();
840
+ state.pageKey = pk;
841
+ state.burstRuns = 0;
842
+ const burst = () => {
843
+ if (getPageKey() !== pk) return;
844
+ if (state.burstRuns >= 6) return;
845
+ state.burstRuns += 1;
846
+ scheduleRun(0);
847
+ // follow-up passes catch late-rendered items (especially on topics)
848
+ window.setTimeout(() => scheduleRun(0), 180);
849
+ window.setTimeout(() => scheduleRun(0), 650);
850
+ window.setTimeout(() => scheduleRun(0), 1400);
851
+ };
852
+ burst();
853
+ }
854
+
855
+
907
856
  // ---------- observers / lifecycle ----------
908
857
 
909
858
  function cleanup() {
@@ -912,7 +861,7 @@ function startShow(id) {
912
861
  // remove all wrappers
913
862
  try {
914
863
  document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
915
- try { el.remove(); } catch (e) {}
864
+ releaseWrapNode(el);
916
865
  });
917
866
  } catch (e) {}
918
867
 
@@ -938,7 +887,7 @@ function startShow(id) {
938
887
  if (state.domObs) return;
939
888
  state.domObs = new MutationObserver(() => {
940
889
  if (state.internalDomChange > 0) return;
941
- if (!isBlocked()) scheduleRun();
890
+ if (!isBlocked()) scheduleBurst();
942
891
  });
943
892
  try {
944
893
  state.domObs.observe(document.body, { childList: true, subtree: true });
@@ -967,13 +916,13 @@ function startShow(id) {
967
916
  insertHeroAdEarly().catch(() => {});
968
917
 
969
918
  // Then normal insertion
970
- scheduleRun();
919
+ scheduleBurst();
971
920
  });
972
921
 
973
922
  // Infinite scroll / partial updates
974
923
  $(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
975
924
  if (isBlocked()) return;
976
- scheduleRun();
925
+ scheduleBurst();
977
926
  });
978
927
  }
979
928
 
@@ -1007,7 +956,7 @@ function startShow(id) {
1007
956
  ticking = true;
1008
957
  window.requestAnimationFrame(() => {
1009
958
  ticking = false;
1010
- if (!isBlocked()) scheduleRun();
959
+ if (!isBlocked()) scheduleBurst();
1011
960
  });
1012
961
  }, { passive: true });
1013
962
  }
@@ -1026,5 +975,5 @@ function startShow(id) {
1026
975
  // First paint: try hero + run
1027
976
  blockedUntil = 0;
1028
977
  insertHeroAdEarly().catch(() => {});
1029
- scheduleRun();
978
+ scheduleBurst();
1030
979
  })();
package/public/style.css CHANGED
@@ -29,17 +29,17 @@
29
29
  display: block !important;
30
30
  margin: 0 !important;
31
31
  padding: 0 !important;
32
- height: 1px !important;
33
- min-height: 1px !important;
32
+ height: 0 !important;
33
+ min-height: 0 !important;
34
34
  overflow: hidden !important;
35
35
  }
36
36
 
37
37
  .nodebb-ezoic-wrap {
38
- min-height: 1px !important;
38
+ min-height: 0 !important;
39
39
  }
40
40
 
41
41
  .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
42
- min-height: 1px !important;
42
+ min-height: 0 !important;
43
43
  }
44
44
 
45
45
  /*