nodebb-plugin-ezoic-infinite 1.4.3 → 1.4.5

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 +92 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
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
@@ -37,6 +37,10 @@
37
37
  pendingById: new Set(),
38
38
  retryById: new Map(),
39
39
  retryTimer: null,
40
+ retryQueue: [],
41
+ retryQueueSet: new Set(),
42
+ retryQueueRunning: false,
43
+ badIds: new Set(),
40
44
 
41
45
  scheduled: false,
42
46
  timer: null,
@@ -165,6 +169,34 @@
165
169
  return { id: null, recycled: null };
166
170
  }
167
171
 
172
+
173
+ function resetPlaceholderInWrap(wrap, id) {
174
+ try {
175
+ if (!wrap) return;
176
+ const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
177
+ if (old) old.remove();
178
+ // Remove any leftover slot markup inside wrapper
179
+ wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins, [id^="ezslot_"]').forEach(n => n.remove());
180
+ const ph = document.createElement('div');
181
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
182
+ wrap.appendChild(ph);
183
+ } catch (e) {}
184
+ }
185
+
186
+ function isAdjacentAd(target) {
187
+ if (!target || !target.nextElementSibling) return false;
188
+ const next = target.nextElementSibling;
189
+ if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
190
+ return false;
191
+ }
192
+
193
+ function isPrevAd(target) {
194
+ if (!target || !target.previousElementSibling) return false;
195
+ const prev = target.previousElementSibling;
196
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
197
+ return false;
198
+ }
199
+
168
200
  function buildWrap(id, kindClass, afterPos) {
169
201
  const wrap = document.createElement('div');
170
202
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
@@ -251,6 +283,43 @@
251
283
  state.retryTimer = setTimeout(refillUnfilled, delay);
252
284
  }
253
285
 
286
+ function enqueueRetry(id) {
287
+ if (!id) return;
288
+ if (state.badIds && state.badIds.has(id)) return;
289
+ if (state.retryQueueSet.has(id)) return;
290
+ state.retryQueueSet.add(id);
291
+ state.retryQueue.push(id);
292
+ processRetryQueue();
293
+ }
294
+
295
+ function processRetryQueue() {
296
+ if (state.retryQueueRunning) return;
297
+ state.retryQueueRunning = true;
298
+
299
+ const step = () => {
300
+ const id = state.retryQueue.shift();
301
+ if (!id) {
302
+ state.retryQueueRunning = false;
303
+ state.badIds = new Set();
304
+ return;
305
+ }
306
+ state.retryQueueSet.delete(id);
307
+ // If this id was previously attempted and still empty, force a full reset before re-requesting.
308
+ const attempts = (state.retryById.get(id) || 0);
309
+ const phNow = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
310
+ const wrapNow = phNow && phNow.parentElement;
311
+ if (attempts > 0 && wrapNow && wrapNow.isConnected && !isPlaceholderFilled(id)) {
312
+ destroyPlaceholderIds([id]);
313
+ resetPlaceholderInWrap(wrapNow, id);
314
+ }
315
+ callShowAdsWhenReady(id);
316
+ setTimeout(step, 1100);
317
+ };
318
+
319
+ step();
320
+ }
321
+
322
+
254
323
  function refillUnfilled() {
255
324
  const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
256
325
  let scheduledAny = false;
@@ -267,13 +336,13 @@
267
336
  }
268
337
 
269
338
  const tries = (state.retryById.get(id) || 0);
270
- if (tries >= 8) continue;
339
+ if (tries >= 8) { state.badIds && state.badIds.add(id); continue; }
271
340
 
272
341
  const r = safeRect(wrap);
273
342
  if (r && (r.top > window.innerHeight + 1200 || r.bottom < -1200)) continue;
274
343
 
275
344
  state.retryById.set(id, tries + 1);
276
- callShowAdsWhenReady(id);
345
+ enqueueRetry(id);
277
346
  scheduledAny = true;
278
347
  }
279
348
 
@@ -392,6 +461,11 @@
392
461
  const el = items[afterPos - 1];
393
462
  if (!el || !el.isConnected) continue;
394
463
 
464
+ // Prevent adjacent ads (DOM-based, robust against virtualization)
465
+ if (isAdjacentAd(el) || isPrevAd(el)) {
466
+ continue;
467
+ }
468
+
395
469
  // Prevent back-to-back at load
396
470
  const prevWrap = findWrap(kindClass, afterPos - 1);
397
471
  if (prevWrap) continue;
@@ -411,6 +485,8 @@
411
485
  destroyPlaceholderIds([id]);
412
486
  wrap = pick.recycled.wrap;
413
487
  if (!moveWrapAfter(wrap, el, kindClass, afterPos)) continue;
488
+ resetPlaceholderInWrap(wrap, id);
489
+ setTimeout(() => { enqueueRetry(id); }, 450);
414
490
  } else {
415
491
  usedSet.add(id);
416
492
  wrap = insertAfter(el, id, kindClass, afterPos);
@@ -418,7 +494,20 @@
418
494
  }
419
495
 
420
496
  liveArr.push({ id, wrap });
421
- callShowAdsWhenReady(id);
497
+ // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
498
+ if (wrap && (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))) {
499
+ try { wrap.remove(); } catch (e) {}
500
+ // Put id back if it was newly consumed (not recycled)
501
+ if (!(pick.recycled && pick.recycled.wrap)) {
502
+ try { kindPool.unshift(id); } catch (e) {}
503
+ try { usedSet.delete(id); } catch (e) {}
504
+ }
505
+ inserted -= 0; // no-op
506
+ continue;
507
+ }
508
+ if (!(pick.recycled && pick.recycled.wrap)) {
509
+ callShowAdsWhenReady(id);
510
+ }
422
511
  inserted += 1;
423
512
  }
424
513
  return inserted;