nodebb-plugin-ezoic-infinite 1.5.23 → 1.5.25

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 +103 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.23",
3
+ "version": "1.5.25",
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
@@ -26,13 +26,13 @@
26
26
  pageKey: null,
27
27
  cfg: null,
28
28
 
29
- poolTopics: [],
30
- poolPosts: [],
31
- poolCategories: [],
32
-
33
- usedTopics: new Set(),
34
- usedPosts: new Set(),
35
- usedCategories: new Set(),
29
+ // Full lists (never consumed) + cursors for round-robin reuse
30
+ allTopics: [],
31
+ allPosts: [],
32
+ allCategories: [],
33
+ curTopics: 0,
34
+ curPosts: 0,
35
+ curCategories: 0,
36
36
 
37
37
  // throttle per placeholder id
38
38
  lastShowById: new Map(),
@@ -46,9 +46,18 @@
46
46
  heroDoneForPage: false,
47
47
  };
48
48
 
49
- const sessionDefinedIds = new Set();
50
49
  const insertingIds = new Set();
51
50
 
51
+ // Debug logs (enable with localStorage.ezoicInfiniteDebug = "1")
52
+ function dbg(...args) {
53
+ try {
54
+ if (window && window.localStorage && window.localStorage.getItem('ezoicInfiniteDebug') === '1') {
55
+ // eslint-disable-next-line no-console
56
+ console.log('[ezoicInfinite]', ...args);
57
+ }
58
+ } catch (e) {}
59
+ }
60
+
52
61
  // ---------- small utils ----------
53
62
 
54
63
  function normalizeBool(v) {
@@ -205,9 +214,9 @@
205
214
 
206
215
  function initPools(cfg) {
207
216
  if (!cfg) return;
208
- if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
209
- if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
210
- if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
217
+ if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
218
+ if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
219
+ if (state.allCategories.length === 0) state.allCategories = parsePool(cfg.categoryPlaceholderIds);
211
220
  }
212
221
 
213
222
  // ---------- insertion primitives ----------
@@ -257,8 +266,50 @@
257
266
  }
258
267
  }
259
268
 
260
- function pickId(pool) {
261
- return pool.length ? pool.shift() : null;
269
+ function pickIdFromAll(allIds, cursorKey) {
270
+ const n = allIds.length;
271
+ if (!n) return null;
272
+
273
+ // Try at most n ids to find one that's not already in the DOM
274
+ for (let tries = 0; tries < n; tries++) {
275
+ const idx = state[cursorKey] % n;
276
+ state[cursorKey] = (state[cursorKey] + 1) % n;
277
+
278
+ const id = allIds[idx];
279
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
280
+ if (ph && ph.isConnected) continue;
281
+
282
+ return id;
283
+ }
284
+ return null;
285
+ }
286
+
287
+
288
+ function removeOneOldWrap(kindClass) {
289
+ try {
290
+ const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
291
+ if (!wraps.length) return false;
292
+
293
+ // Prefer a wrap far above the viewport
294
+ let victim = null;
295
+ for (const w of wraps) {
296
+ const r = w.getBoundingClientRect();
297
+ if (r.bottom < -2000) { victim = w; break; }
298
+ }
299
+ // Otherwise remove the earliest one in the document
300
+ if (!victim) victim = wraps[0];
301
+
302
+ // Unobserve placeholder if still observed
303
+ try {
304
+ const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
305
+ if (ph && state.io) state.io.unobserve(ph);
306
+ } catch (e) {}
307
+
308
+ victim.remove();
309
+ return true;
310
+ } catch (e) {
311
+ return false;
312
+ }
262
313
  }
263
314
 
264
315
  function showAd(id) {
@@ -280,7 +331,6 @@
280
331
  // Fast path
281
332
  if (typeof ez.showAds === 'function') {
282
333
  ez.showAds(id);
283
- sessionDefinedIds.add(id);
284
334
  return;
285
335
  }
286
336
 
@@ -294,7 +344,6 @@
294
344
  const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
295
345
  if (!el || !el.isConnected) return;
296
346
  window.ezstandalone.showAds(id);
297
- sessionDefinedIds.add(id);
298
347
  } catch (e) {}
299
348
  });
300
349
  }
@@ -348,7 +397,7 @@
348
397
  return Array.from(new Set(out)).sort((a, b) => a - b);
349
398
  }
350
399
 
351
- function injectBetween(kindClass, items, interval, showFirst, pool, usedSet) {
400
+ function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
352
401
  if (!items.length) return 0;
353
402
 
354
403
  const targets = computeTargets(items.length, interval, showFirst);
@@ -362,14 +411,16 @@
362
411
  if (isAdjacentAd(el)) continue;
363
412
  if (findWrap(kindClass, afterPos)) continue;
364
413
 
365
- const id = pickId(pool);
414
+ let id = pickIdFromAll(allIds, cursorKey);
415
+ if (!id) {
416
+ // No free ids: recycle an old ad wrapper so we can reuse its placeholder id
417
+ const recycled = removeOneOldWrap(kindClass);
418
+ dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
419
+ id = pickIdFromAll(allIds, cursorKey);
420
+ }
366
421
  if (!id) break;
367
-
368
- usedSet.add(id);
369
422
  const wrap = insertAfter(el, id, kindClass, afterPos);
370
423
  if (!wrap) {
371
- usedSet.delete(id);
372
- pool.unshift(id);
373
424
  continue;
374
425
  }
375
426
 
@@ -383,36 +434,42 @@
383
434
  async function insertHeroAdEarly() {
384
435
  if (state.heroDoneForPage) return;
385
436
  const cfg = await fetchConfigOnce();
386
- if (!cfg || cfg.excluded) return;
437
+ if (!cfg) { dbg('no-config'); return; }
438
+ if (cfg.excluded) { dbg('excluded'); return; }
387
439
 
388
440
  initPools(cfg);
389
441
 
390
442
  const kind = getKind();
391
443
  let items = [];
392
- let pool = null;
393
- let usedSet = null;
444
+ let allIds = [];
445
+ let cursorKey = '';
394
446
  let kindClass = '';
447
+ let showFirst = false;
395
448
 
396
449
  if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
397
450
  items = getPostContainers();
398
- pool = state.poolPosts;
399
- usedSet = state.usedPosts;
451
+ allIds = state.allPosts;
452
+ cursorKey = 'curPosts';
400
453
  kindClass = 'ezoic-ad-message';
454
+ showFirst = normalizeBool(cfg.showFirstMessageAd);
401
455
  } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
402
456
  items = getTopicItems();
403
- pool = state.poolTopics;
404
- usedSet = state.usedTopics;
457
+ allIds = state.allTopics;
458
+ cursorKey = 'curTopics';
405
459
  kindClass = 'ezoic-ad-between';
460
+ showFirst = normalizeBool(cfg.showFirstTopicAd);
406
461
  } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
407
462
  items = getCategoryItems();
408
- pool = state.poolCategories;
409
- usedSet = state.usedCategories;
463
+ allIds = state.allCategories;
464
+ cursorKey = 'curCategories';
410
465
  kindClass = 'ezoic-ad-categories';
466
+ showFirst = normalizeBool(cfg.showFirstCategoryAd);
411
467
  } else {
412
468
  return;
413
469
  }
414
470
 
415
471
  if (!items.length) return;
472
+ if (!showFirst) { state.heroDoneForPage = true; return; }
416
473
 
417
474
  // Insert after the very first item (above-the-fold)
418
475
  const afterPos = 1;
@@ -421,14 +478,11 @@
421
478
  if (isAdjacentAd(el)) return;
422
479
  if (findWrap(kindClass, afterPos)) { state.heroDoneForPage = true; return; }
423
480
 
424
- const id = pickId(pool);
481
+ const id = pickIdFromAll(allIds, cursorKey);
425
482
  if (!id) return;
426
483
 
427
- usedSet.add(id);
428
484
  const wrap = insertAfter(el, id, kindClass, afterPos);
429
485
  if (!wrap) {
430
- usedSet.delete(id);
431
- pool.unshift(id);
432
486
  return;
433
487
  }
434
488
 
@@ -437,12 +491,13 @@
437
491
  }
438
492
 
439
493
  async function runCore() {
440
- if (EZOIC_BLOCKED) return;
494
+ if (EZOIC_BLOCKED) { dbg('blocked'); return; }
441
495
 
442
496
  patchShowAds();
443
497
 
444
498
  const cfg = await fetchConfigOnce();
445
- if (!cfg || cfg.excluded) return;
499
+ if (!cfg) { dbg('no-config'); return; }
500
+ if (cfg.excluded) { dbg('excluded'); return; }
446
501
  initPools(cfg);
447
502
 
448
503
  const kind = getKind();
@@ -454,8 +509,8 @@
454
509
  getPostContainers(),
455
510
  Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
456
511
  normalizeBool(cfg.showFirstMessageAd),
457
- state.poolPosts,
458
- state.usedPosts
512
+ state.allPosts,
513
+ 'curPosts'
459
514
  );
460
515
  }
461
516
  } else if (kind === 'categoryTopics') {
@@ -465,8 +520,8 @@
465
520
  getTopicItems(),
466
521
  Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
467
522
  normalizeBool(cfg.showFirstTopicAd),
468
- state.poolTopics,
469
- state.usedTopics
523
+ state.allTopics,
524
+ 'curTopics'
470
525
  );
471
526
  }
472
527
  } else if (kind === 'categories') {
@@ -476,8 +531,8 @@
476
531
  getCategoryItems(),
477
532
  Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
478
533
  normalizeBool(cfg.showFirstCategoryAd),
479
- state.poolCategories,
480
- state.usedCategories
534
+ state.allCategories,
535
+ 'curCategories'
481
536
  );
482
537
  }
483
538
  }
@@ -508,17 +563,15 @@
508
563
 
509
564
  // reset state
510
565
  state.cfg = null;
511
- state.poolTopics = [];
512
- state.poolPosts = [];
513
- state.poolCategories = [];
514
- state.usedTopics.clear();
515
- state.usedPosts.clear();
516
- state.usedCategories.clear();
566
+ state.allTopics = [];
567
+ state.allPosts = [];
568
+ state.allCategories = [];
569
+ state.curTopics = 0;
570
+ state.curPosts = 0;
571
+ state.curCategories = 0;
517
572
  state.lastShowById.clear();
518
573
  state.heroDoneForPage = false;
519
574
 
520
- sessionDefinedIds.clear();
521
-
522
575
  // keep observers alive (MutationObserver will re-trigger after navigation)
523
576
  }
524
577