nodebb-plugin-ezoic-infinite 1.5.66 → 1.5.67

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.67",
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,20 +95,11 @@
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; }
113
- }
114
-
115
-
116
103
 
117
104
  function markEmptyWrapper(id) {
118
105
  try {
@@ -130,17 +117,6 @@
130
117
  // consider empty if only whitespace and no iframes/ins/img
131
118
  const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
132
119
  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
120
  } catch (e) {}
145
121
  }, 3500);
146
122
  } catch (e) {}
@@ -259,40 +235,22 @@
259
235
  window.__nodebbEzoicPatched = true;
260
236
  const orig = ez.showAds;
261
237
 
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
238
  ez.showAds = function (...args) {
266
239
  if (isBlocked()) return;
267
240
 
268
- const now = Date.now();
269
241
  let ids = [];
270
- const isArrayCall = (args.length === 1 && Array.isArray(args[0]));
271
- if (isArrayCall) ids = args[0];
242
+ if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
272
243
  else ids = args;
273
244
 
274
- const filtered = [];
275
245
  const seen = new Set();
276
246
  for (const v of ids) {
277
247
  const id = parseInt(v, 10);
278
248
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
279
249
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
280
250
  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
251
  seen.add(id);
288
- filtered.push(id);
252
+ try { orig.call(ez, id); } catch (e) {}
289
253
  }
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
254
  };
297
255
  } catch (e) {}
298
256
  };
@@ -390,21 +348,11 @@ function withInternalDomChange(fn) {
390
348
  // NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
391
349
  let ok = false;
392
350
  let prev = wrap.previousElementSibling;
393
- for (let i = 0; i < 8 && prev; i++) {
351
+ for (let i = 0; i < 3 && prev; i++) {
394
352
  if (itemSet.has(prev)) { ok = true; break; }
395
353
  prev = prev.previousElementSibling;
396
354
  }
397
355
 
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
- 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
356
  if (!ok) {
409
357
  const id = getWrapIdFromWrap(wrap);
410
358
  withInternalDomChange(() => {
@@ -421,28 +369,6 @@ function withInternalDomChange(fn) {
421
369
  return removed;
422
370
  }
423
371
 
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
372
  function refreshEmptyState(id) {
447
373
  // After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
448
374
  window.setTimeout(() => {
@@ -458,25 +384,17 @@ function withInternalDomChange(fn) {
458
384
  }, 3500);
459
385
  }
460
386
 
461
- function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
387
+ function buildWrap(id, kindClass, afterPos) {
462
388
  const wrap = document.createElement('div');
463
389
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
464
390
  wrap.setAttribute('data-ezoic-after', String(afterPos));
465
391
  wrap.setAttribute('data-ezoic-wrapid', String(id));
466
392
  wrap.style.width = '100%';
467
393
 
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 {
475
- const ph = document.createElement('div');
476
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
477
- ph.setAttribute('data-ezoic-id', String(id));
478
- wrap.appendChild(ph);
479
- }
394
+ const ph = document.createElement('div');
395
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
396
+ ph.setAttribute('data-ezoic-id', String(id));
397
+ wrap.appendChild(ph);
480
398
 
481
399
  return wrap;
482
400
  }
@@ -490,28 +408,24 @@ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
490
408
  if (findWrap(kindClass, afterPos)) return null;
491
409
  if (insertingIds.has(id)) return null;
492
410
 
411
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
412
+
493
413
  insertingIds.add(id);
494
414
  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.
415
+ const wrap = buildWrap(id, kindClass, afterPos);
416
+ target.insertAdjacentElement('afterend', wrap);
417
+
418
+ // If a placeholder with this id already exists elsewhere (some Ezoic flows
419
+ // pre-create placeholders), move it into our wrapper instead of aborting.
420
+ // replaceChild moves the node atomically (no detach window).
421
+ if (existingPh && existingPh !== wrap.firstElementChild) {
505
422
  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) {}
423
+ existingPh.setAttribute('data-ezoic-id', String(id));
424
+ wrap.replaceChild(existingPh, wrap.firstElementChild);
425
+ } catch (e) {
426
+ // Keep the new placeholder if replace fails.
427
+ }
511
428
  }
512
-
513
- const wrap = buildWrap(id, kindClass, afterPos, moved);
514
- target.insertAdjacentElement('afterend', wrap);
515
429
  return wrap;
516
430
  } finally {
517
431
  insertingIds.delete(id);
@@ -711,6 +625,15 @@ function startShow(id) {
711
625
  if (i % interval === 0) out.push(i);
712
626
  }
713
627
  return Array.from(new Set(out)).sort((a, b) => a - b);
628
+ // If we inserted the maximum batch, likely there are more targets.
629
+ // Schedule a follow-up pass (bounded via scheduleRun coalescing).
630
+ const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
631
+ if (insertedThisRun >= maxInserts) {
632
+ scheduleRun(120);
633
+ scheduleRun(420);
634
+ }
635
+ }
636
+
714
637
  }
715
638
 
716
639
  function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
@@ -720,10 +643,8 @@ function startShow(id) {
720
643
  let inserted = 0;
721
644
  const maxInserts = MAX_INSERTS_PER_RUN + (isBoosted() ? 1 : 0);
722
645
 
723
- let hitLimit = false;
724
-
725
646
  for (const afterPos of targets) {
726
- if (inserted >= maxInserts) { hitLimit = true; break; }
647
+ if (inserted >= maxInserts) break;
727
648
 
728
649
  const el = items[afterPos - 1];
729
650
  if (!el || !el.isConnected) continue;
@@ -755,11 +676,6 @@ function startShow(id) {
755
676
  inserted += 1;
756
677
  }
757
678
 
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
679
  return inserted;
764
680
  }
765
681
 
@@ -771,8 +687,6 @@ function startShow(id) {
771
687
 
772
688
  initPools(cfg);
773
689
 
774
- state.needsMoreRun = false;
775
-
776
690
  const kind = getKind();
777
691
  let items = [];
778
692
  let allIds = [];
@@ -826,6 +740,7 @@ function startShow(id) {
826
740
 
827
741
  async function runCore() {
828
742
  if (isBlocked()) { dbg('blocked'); return; }
743
+ let insertedThisRun = 0;
829
744
 
830
745
  patchShowAds();
831
746
 
@@ -840,7 +755,7 @@ function startShow(id) {
840
755
  if (normalizeBool(cfg.enableMessageAds)) {
841
756
  const __items = getPostContainers();
842
757
  pruneOrphanWraps('ezoic-ad-message', __items);
843
- injectBetween(
758
+ insertedThisRun += injectBetween(
844
759
  'ezoic-ad-message',
845
760
  __items,
846
761
  Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
@@ -848,13 +763,12 @@ function startShow(id) {
848
763
  state.allPosts,
849
764
  'curPosts'
850
765
  );
851
- declusterWraps('ezoic-ad-message');
852
766
  }
853
767
  } else if (kind === 'categoryTopics') {
854
768
  if (normalizeBool(cfg.enableBetweenAds)) {
855
769
  const __items = getTopicItems();
856
770
  pruneOrphanWraps('ezoic-ad-between', __items);
857
- injectBetween(
771
+ insertedThisRun += injectBetween(
858
772
  'ezoic-ad-between',
859
773
  __items,
860
774
  Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
@@ -862,13 +776,12 @@ function startShow(id) {
862
776
  state.allTopics,
863
777
  'curTopics'
864
778
  );
865
- declusterWraps('ezoic-ad-between');
866
779
  }
867
780
  } else if (kind === 'categories') {
868
781
  if (normalizeBool(cfg.enableCategoryAds)) {
869
782
  const __items = getCategoryItems();
870
783
  pruneOrphanWraps('ezoic-ad-categories', __items);
871
- injectBetween(
784
+ insertedThisRun += injectBetween(
872
785
  'ezoic-ad-categories',
873
786
  __items,
874
787
  Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
@@ -876,34 +789,47 @@ function startShow(id) {
876
789
  state.allCategories,
877
790
  'curCategories'
878
791
  );
879
- declusterWraps('ezoic-ad-categories');
880
- }
881
- }
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
792
  }
892
793
  }
893
-
894
794
  }
895
795
 
896
- function scheduleRun() {
796
+ function scheduleRun(delayMs = 0) {
797
+ // schedule a single run (coalesced)
897
798
  if (state.runQueued) return;
898
799
  state.runQueued = true;
899
- window.requestAnimationFrame(() => {
800
+ const doRun = () => {
900
801
  state.runQueued = false;
901
802
  const pk = getPageKey();
902
803
  if (state.pageKey && pk !== state.pageKey) return;
903
804
  runCore().catch(() => {});
904
- });
805
+ };
806
+ if (delayMs > 0) {
807
+ window.setTimeout(() => window.requestAnimationFrame(doRun), delayMs);
808
+ } else {
809
+ window.requestAnimationFrame(doRun);
810
+ }
811
+ }
812
+
813
+ function scheduleBurst() {
814
+ // During ajaxify/infinite scroll, the DOM may arrive in waves.
815
+ // We run a small, bounded burst to ensure all 1/X targets are reached.
816
+ const pk = getPageKey();
817
+ state.pageKey = pk;
818
+ state.burstRuns = 0;
819
+ const burst = () => {
820
+ if (getPageKey() !== pk) return;
821
+ if (state.burstRuns >= 6) return;
822
+ state.burstRuns += 1;
823
+ scheduleRun(0);
824
+ // follow-up passes catch late-rendered items (especially on topics)
825
+ window.setTimeout(() => scheduleRun(0), 180);
826
+ window.setTimeout(() => scheduleRun(0), 650);
827
+ window.setTimeout(() => scheduleRun(0), 1400);
828
+ };
829
+ burst();
905
830
  }
906
831
 
832
+
907
833
  // ---------- observers / lifecycle ----------
908
834
 
909
835
  function cleanup() {
@@ -938,7 +864,7 @@ function startShow(id) {
938
864
  if (state.domObs) return;
939
865
  state.domObs = new MutationObserver(() => {
940
866
  if (state.internalDomChange > 0) return;
941
- if (!isBlocked()) scheduleRun();
867
+ if (!isBlocked()) scheduleBurst();
942
868
  });
943
869
  try {
944
870
  state.domObs.observe(document.body, { childList: true, subtree: true });
@@ -967,13 +893,13 @@ function startShow(id) {
967
893
  insertHeroAdEarly().catch(() => {});
968
894
 
969
895
  // Then normal insertion
970
- scheduleRun();
896
+ scheduleBurst();
971
897
  });
972
898
 
973
899
  // Infinite scroll / partial updates
974
900
  $(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
975
901
  if (isBlocked()) return;
976
- scheduleRun();
902
+ scheduleBurst();
977
903
  });
978
904
  }
979
905
 
@@ -1007,7 +933,7 @@ function startShow(id) {
1007
933
  ticking = true;
1008
934
  window.requestAnimationFrame(() => {
1009
935
  ticking = false;
1010
- if (!isBlocked()) scheduleRun();
936
+ if (!isBlocked()) scheduleBurst();
1011
937
  });
1012
938
  }, { passive: true });
1013
939
  }
@@ -1026,5 +952,5 @@ function startShow(id) {
1026
952
  // First paint: try hero + run
1027
953
  blockedUntil = 0;
1028
954
  insertHeroAdEarly().catch(() => {});
1029
- scheduleRun();
955
+ scheduleBurst();
1030
956
  })();
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
  /*