nodebb-plugin-ezoic-infinite 1.5.65 → 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.65",
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
@@ -95,20 +95,11 @@
95
95
 
96
96
  // hero)
97
97
  heroDoneForPage: false,
98
+ burstRuns: 0,
98
99
  };
99
100
 
100
101
  const insertingIds = new Set();
101
102
 
102
- function unemptyIfFilled(wrap, ph) {
103
- try {
104
- if (!wrap || !ph) return false;
105
- const hasAd = !!(ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
106
- if (hasAd) wrap.classList.remove('is-empty');
107
- return hasAd;
108
- } catch (e) { return false; }
109
- }
110
-
111
-
112
103
 
113
104
  function markEmptyWrapper(id) {
114
105
  try {
@@ -126,17 +117,6 @@
126
117
  // consider empty if only whitespace and no iframes/ins/img
127
118
  const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
128
119
  if (!hasAd) w2.classList.add('is-empty');
129
- // If the ad fills later, immediately uncollapse to avoid "missing ads" perception.
130
- if (!unemptyIfFilled(w2, ph2)) {
131
- try {
132
- const mo = new MutationObserver(() => {
133
- if (unemptyIfFilled(w2, ph2)) { try { mo.disconnect(); } catch (e) {} }
134
- });
135
- mo.observe(ph2, { childList: true, subtree: true });
136
- // safety stop
137
- setTimeout(() => { try { mo.disconnect(); } catch (e) {} }, 30000);
138
- } catch (e) {}
139
- }
140
120
  } catch (e) {}
141
121
  }, 3500);
142
122
  } catch (e) {}
@@ -255,40 +235,22 @@
255
235
  window.__nodebbEzoicPatched = true;
256
236
  const orig = ez.showAds;
257
237
 
258
- // Important: preserve the original calling convention.
259
- // Some Ezoic builds expect an array; calling one-by-one can lead to
260
- // repeated define attempts and "Placeholder Id ... already been defined".
261
238
  ez.showAds = function (...args) {
262
239
  if (isBlocked()) return;
263
240
 
264
- const now = Date.now();
265
241
  let ids = [];
266
- const isArrayCall = (args.length === 1 && Array.isArray(args[0]));
267
- if (isArrayCall) ids = args[0];
242
+ if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
268
243
  else ids = args;
269
244
 
270
- const filtered = [];
271
245
  const seen = new Set();
272
246
  for (const v of ids) {
273
247
  const id = parseInt(v, 10);
274
248
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
275
249
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
276
250
  if (!ph || !ph.isConnected) continue;
277
-
278
- // Extra throttle to avoid rapid duplicate defines during ajaxify churn
279
- const last = state.lastShowById.get(id) || 0;
280
- if (now - last < 650) continue;
281
- state.lastShowById.set(id, now);
282
-
283
251
  seen.add(id);
284
- filtered.push(id);
252
+ try { orig.call(ez, id); } catch (e) {}
285
253
  }
286
-
287
- if (!filtered.length) return;
288
- try {
289
- if (isArrayCall) orig.call(ez, filtered);
290
- else orig.apply(ez, filtered);
291
- } catch (e) {}
292
254
  };
293
255
  } catch (e) {}
294
256
  };
@@ -386,21 +348,11 @@ function withInternalDomChange(fn) {
386
348
  // NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
387
349
  let ok = false;
388
350
  let prev = wrap.previousElementSibling;
389
- for (let i = 0; i < 8 && prev; i++) {
351
+ for (let i = 0; i < 3 && prev; i++) {
390
352
  if (itemSet.has(prev)) { ok = true; break; }
391
353
  prev = prev.previousElementSibling;
392
354
  }
393
355
 
394
- // If it is already filled (iframe/ins/img), be conservative and keep it.
395
- // Prevents ads "disappearing too fast" during ajaxify churn / minor rerenders.
396
- if (!ok) {
397
- try {
398
- const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
399
- const filled = !!(ph && ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
400
- if (filled) ok = true;
401
- } catch (e) {}
402
- }
403
-
404
356
  if (!ok) {
405
357
  const id = getWrapIdFromWrap(wrap);
406
358
  withInternalDomChange(() => {
@@ -417,28 +369,6 @@ function withInternalDomChange(fn) {
417
369
  return removed;
418
370
  }
419
371
 
420
- function declusterWraps(kindClass) {
421
- try {
422
- const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
423
- if (wraps.length < 2) return;
424
- for (let i = 1; i < wraps.length; i++) {
425
- const w = wraps[i];
426
- if (!w || !w.isConnected) continue;
427
- // If previous siblings contain another wrap within 2 hops, remove this one.
428
- let prev = w.previousElementSibling;
429
- let hops = 0;
430
- while (prev && hops < 3) {
431
- if (prev.classList && prev.classList.contains(WRAP_CLASS)) {
432
- withInternalDomChange(() => { try { w.remove(); } catch (e) {} });
433
- break;
434
- }
435
- prev = prev.previousElementSibling;
436
- hops++;
437
- }
438
- }
439
- } catch (e) {}
440
- }
441
-
442
372
  function refreshEmptyState(id) {
443
373
  // After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
444
374
  window.setTimeout(() => {
@@ -454,25 +384,17 @@ function withInternalDomChange(fn) {
454
384
  }, 3500);
455
385
  }
456
386
 
457
- function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
387
+ function buildWrap(id, kindClass, afterPos) {
458
388
  const wrap = document.createElement('div');
459
389
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
460
390
  wrap.setAttribute('data-ezoic-after', String(afterPos));
461
391
  wrap.setAttribute('data-ezoic-wrapid', String(id));
462
392
  wrap.style.width = '100%';
463
393
 
464
- if (existingPlaceholder && existingPlaceholder.nodeType === 1) {
465
- try {
466
- existingPlaceholder.id = `${PLACEHOLDER_PREFIX}${id}`;
467
- existingPlaceholder.setAttribute('data-ezoic-id', String(id));
468
- } catch (e) {}
469
- wrap.appendChild(existingPlaceholder);
470
- } else {
471
- const ph = document.createElement('div');
472
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
473
- ph.setAttribute('data-ezoic-id', String(id));
474
- wrap.appendChild(ph);
475
- }
394
+ const ph = document.createElement('div');
395
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
396
+ ph.setAttribute('data-ezoic-id', String(id));
397
+ wrap.appendChild(ph);
476
398
 
477
399
  return wrap;
478
400
  }
@@ -486,28 +408,24 @@ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
486
408
  if (findWrap(kindClass, afterPos)) return null;
487
409
  if (insertingIds.has(id)) return null;
488
410
 
411
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
412
+
489
413
  insertingIds.add(id);
490
414
  try {
491
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
492
-
493
- // CRITICAL: never create a second element with the same id, even briefly.
494
- // That can trigger "Placeholder Id ... already been defined" during load.
495
- // If an existing placeholder already exists, move it into the new wrapper
496
- // before inserting the wrapper into the DOM.
497
- let moved = null;
498
- if (existingPh && existingPh.isConnected) {
499
- moved = existingPh;
500
- // 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) {
501
422
  try {
502
- const oldWrap = moved.closest && moved.closest(`.${WRAP_CLASS}`);
503
- if (oldWrap && oldWrap.parentNode) {
504
- withInternalDomChange(() => { try { oldWrap.remove(); } catch (e) {} });
505
- }
506
- } 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
+ }
507
428
  }
508
-
509
- const wrap = buildWrap(id, kindClass, afterPos, moved);
510
- target.insertAdjacentElement('afterend', wrap);
511
429
  return wrap;
512
430
  } finally {
513
431
  insertingIds.delete(id);
@@ -707,6 +625,15 @@ function startShow(id) {
707
625
  if (i % interval === 0) out.push(i);
708
626
  }
709
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
+
710
637
  }
711
638
 
712
639
  function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
@@ -813,6 +740,7 @@ function startShow(id) {
813
740
 
814
741
  async function runCore() {
815
742
  if (isBlocked()) { dbg('blocked'); return; }
743
+ let insertedThisRun = 0;
816
744
 
817
745
  patchShowAds();
818
746
 
@@ -827,7 +755,7 @@ function startShow(id) {
827
755
  if (normalizeBool(cfg.enableMessageAds)) {
828
756
  const __items = getPostContainers();
829
757
  pruneOrphanWraps('ezoic-ad-message', __items);
830
- injectBetween(
758
+ insertedThisRun += injectBetween(
831
759
  'ezoic-ad-message',
832
760
  __items,
833
761
  Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
@@ -835,13 +763,12 @@ function startShow(id) {
835
763
  state.allPosts,
836
764
  'curPosts'
837
765
  );
838
- declusterWraps('ezoic-ad-message');
839
766
  }
840
767
  } else if (kind === 'categoryTopics') {
841
768
  if (normalizeBool(cfg.enableBetweenAds)) {
842
769
  const __items = getTopicItems();
843
770
  pruneOrphanWraps('ezoic-ad-between', __items);
844
- injectBetween(
771
+ insertedThisRun += injectBetween(
845
772
  'ezoic-ad-between',
846
773
  __items,
847
774
  Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
@@ -849,13 +776,12 @@ function startShow(id) {
849
776
  state.allTopics,
850
777
  'curTopics'
851
778
  );
852
- declusterWraps('ezoic-ad-between');
853
779
  }
854
780
  } else if (kind === 'categories') {
855
781
  if (normalizeBool(cfg.enableCategoryAds)) {
856
782
  const __items = getCategoryItems();
857
783
  pruneOrphanWraps('ezoic-ad-categories', __items);
858
- injectBetween(
784
+ insertedThisRun += injectBetween(
859
785
  'ezoic-ad-categories',
860
786
  __items,
861
787
  Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
@@ -863,22 +789,47 @@ function startShow(id) {
863
789
  state.allCategories,
864
790
  'curCategories'
865
791
  );
866
- declusterWraps('ezoic-ad-categories');
867
792
  }
868
793
  }
869
794
  }
870
795
 
871
- function scheduleRun() {
796
+ function scheduleRun(delayMs = 0) {
797
+ // schedule a single run (coalesced)
872
798
  if (state.runQueued) return;
873
799
  state.runQueued = true;
874
- window.requestAnimationFrame(() => {
800
+ const doRun = () => {
875
801
  state.runQueued = false;
876
802
  const pk = getPageKey();
877
803
  if (state.pageKey && pk !== state.pageKey) return;
878
804
  runCore().catch(() => {});
879
- });
805
+ };
806
+ if (delayMs > 0) {
807
+ window.setTimeout(() => window.requestAnimationFrame(doRun), delayMs);
808
+ } else {
809
+ window.requestAnimationFrame(doRun);
810
+ }
880
811
  }
881
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();
830
+ }
831
+
832
+
882
833
  // ---------- observers / lifecycle ----------
883
834
 
884
835
  function cleanup() {
@@ -913,7 +864,7 @@ function startShow(id) {
913
864
  if (state.domObs) return;
914
865
  state.domObs = new MutationObserver(() => {
915
866
  if (state.internalDomChange > 0) return;
916
- if (!isBlocked()) scheduleRun();
867
+ if (!isBlocked()) scheduleBurst();
917
868
  });
918
869
  try {
919
870
  state.domObs.observe(document.body, { childList: true, subtree: true });
@@ -942,13 +893,13 @@ function startShow(id) {
942
893
  insertHeroAdEarly().catch(() => {});
943
894
 
944
895
  // Then normal insertion
945
- scheduleRun();
896
+ scheduleBurst();
946
897
  });
947
898
 
948
899
  // Infinite scroll / partial updates
949
900
  $(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
950
901
  if (isBlocked()) return;
951
- scheduleRun();
902
+ scheduleBurst();
952
903
  });
953
904
  }
954
905
 
@@ -982,7 +933,7 @@ function startShow(id) {
982
933
  ticking = true;
983
934
  window.requestAnimationFrame(() => {
984
935
  ticking = false;
985
- if (!isBlocked()) scheduleRun();
936
+ if (!isBlocked()) scheduleBurst();
986
937
  });
987
938
  }, { passive: true });
988
939
  }
@@ -1001,5 +952,5 @@ function startShow(id) {
1001
952
  // First paint: try hero + run
1002
953
  blockedUntil = 0;
1003
954
  insertHeroAdEarly().catch(() => {});
1004
- scheduleRun();
955
+ scheduleBurst();
1005
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
  /*