nodebb-plugin-ezoic-infinite 1.8.62 → 1.8.63

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/library.js CHANGED
@@ -141,12 +141,13 @@ const HEAD_PRECONNECTS = [
141
141
  const EZOIC_SCRIPTS = [
142
142
  '<script data-cfasync="false" src="https://cmp.gatekeeperconsent.com/min.js"></script>',
143
143
  '<script data-cfasync="false" src="https://the.gatekeeperconsent.com/cmp.min.js"></script>',
144
+ '<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
144
145
  '<script>',
145
146
  'window._ezaq = window._ezaq || {};',
146
147
  'window.ezstandalone = window.ezstandalone || {};',
147
148
  'ezstandalone.cmd = ezstandalone.cmd || [];',
148
149
  '</script>',
149
- '<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
150
+ '<script src="//ezoicanalytics.com/analytics.js"></script>',
150
151
  ].join('\n');
151
152
 
152
153
  // ── Hooks ────────────────────────────────────────────────────────────────────
@@ -234,4 +235,4 @@ plugin.init = async ({ router, middleware }) => {
234
235
  });
235
236
  };
236
237
 
237
- module.exports = plugin;
238
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.62",
3
+ "version": "1.8.63",
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
@@ -1,12 +1,12 @@
1
1
  /**
2
- * NodeBB Ezoic Infinite Ads — client.js v2.2.0
2
+ * NodeBB Ezoic Infinite Ads — client.js v2.3.0
3
3
  *
4
- * Based on working v2-fixed with targeted fixes:
5
- * - Ezoic API: showAds() + destroyPlaceholders() only (per official docs)
6
- * - cleanup(): destroyAll() before DOM removal, only on real page change
7
- * - Recycling: conservative threshold (-3vh), age/inflight guards
8
- * - aria-hidden: MutationObserver protection
9
- * - Empty check: more conservative timing and GPT slot awareness
4
+ * Architecture based on battle-tested v50, with targeted improvements:
5
+ * - Ezoic API: showAds() + destroyPlaceholders() per official docs
6
+ * - wrapsByClass Set for O(1) recycle lookup (no querySelectorAll)
7
+ * - MutationObserver: ad fill detection + virtualization re-observe
8
+ * - Conservative empty check (30s/60s, GPT-aware)
9
+ * - aria-hidden + TCF locator protection
10
10
  */
11
11
  (function nbbEzoicInfinite() {
12
12
  'use strict';
@@ -24,30 +24,25 @@
24
24
  };
25
25
 
26
26
  const TIMING = {
27
- EMPTY_CHECK_EARLY_MS: 30_000,
28
- EMPTY_CHECK_LATE_MS: 60_000,
29
- MIN_PRUNE_AGE_MS: 8_000,
30
- SHOW_THROTTLE_MS: 900,
31
- BURST_COOLDOWN_MS: 200,
32
- BLOCK_DURATION_MS: 1_500,
33
- SHOW_TIMEOUT_MS: 7_000,
34
- SHOW_RELEASE_MS: 700,
35
- BATCH_FLUSH_MS: 80,
36
- RECYCLE_DELAY_MS: 450,
27
+ EMPTY_CHECK_MS_1: 30_000,
28
+ EMPTY_CHECK_MS_2: 60_000,
29
+ MIN_PRUNE_AGE_MS: 8_000,
30
+ RECYCLE_MIN_AGE_MS: 5_000,
31
+ SHOW_THROTTLE_MS: 900,
32
+ BURST_COOLDOWN_MS: 200,
33
+ BLOCK_DURATION_MS: 1_500,
34
+ SHOW_TIMEOUT_MS: 7_000,
35
+ SHOW_RELEASE_MS: 700,
36
+ RECYCLE_DELAY_MS: 450,
37
37
  };
38
38
 
39
- const LIMITS = {
40
- MAX_INSERTS_RUN: 6,
41
- MAX_INFLIGHT: 4,
42
- BATCH_SIZE: 3,
43
- MAX_BURST_STEPS: 8,
44
- BURST_WINDOW_MS: 2_000,
45
- };
39
+ const MAX_INSERTS_RUN = 6;
40
+ const MAX_INFLIGHT = 4;
41
+ const MAX_BURST_STEPS = 8;
42
+ const BURST_WINDOW_MS = 2_000;
46
43
 
47
- const IO_MARGIN = {
48
- DESKTOP: '2500px 0px 2500px 0px',
49
- MOBILE: '3500px 0px 3500px 0px',
50
- };
44
+ const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
45
+ const IO_MARGIN_MOBILE = '3500px 0px 3500px 0px';
51
46
 
52
47
  const SEL = {
53
48
  post: '[component="post"][data-pid]',
@@ -62,39 +57,26 @@
62
57
  };
63
58
 
64
59
  const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
65
- const RECYCLE_MIN_AGE_MS = 5_000;
66
60
 
67
61
  // ── Utility ────────────────────────────────────────────────────────────────
68
62
 
69
63
  const now = () => Date.now();
70
64
  const isMobile = () => window.innerWidth < 768;
71
65
  const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
72
-
73
- function isFilled(node) {
74
- return node?.querySelector?.(FILL_SEL) != null;
75
- }
76
-
77
- function isPlaceholderUsed(ph) {
78
- if (!ph?.isConnected) return false;
79
- return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
80
- }
66
+ const isFilled = n => !!(n?.querySelector?.(FILL_SEL));
81
67
 
82
68
  function parseIds(raw) {
83
- const out = [];
84
- const seen = new Set();
85
- for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
86
- const n = parseInt(line, 10);
87
- if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
88
- seen.add(n);
89
- out.push(n);
90
- }
69
+ const out = [], seen = new Set();
70
+ for (const v of String(raw || '').split(/[\r\n,\s]+/)) {
71
+ const n = parseInt(v, 10);
72
+ if (n > 0 && Number.isFinite(n) && !seen.has(n)) { seen.add(n); out.push(n); }
91
73
  }
92
74
  return out;
93
75
  }
94
76
 
95
77
  // ── State ──────────────────────────────────────────────────────────────────
96
78
 
97
- const state = {
79
+ const S = {
98
80
  pageKey: null,
99
81
  kind: null,
100
82
  cfg: null,
@@ -103,18 +85,17 @@
103
85
  pools: { topics: [], posts: [], categories: [] },
104
86
  cursors: { topics: 0, posts: 0, categories: 0 },
105
87
 
106
- mountedIds: new Set(),
107
- phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
108
- lastShow: new Map(), // id → timestamp
88
+ mountedIds: new Set(),
89
+ lastShow: new Map(),
109
90
 
110
- wrapByKey: new Map(),
111
- wrapsByClass: new Map(),
91
+ wrapByKey: new Map(), // anchorKey → wrap element
92
+ wrapsByClass: new Map(), // kindClass → Set<wrap>
112
93
 
113
94
  io: null,
114
95
  domObs: null,
115
96
 
116
- mutGuard: 0,
117
- blockedUntil: 0,
97
+ mutGuard: 0,
98
+ blockedUntil: 0,
118
99
 
119
100
  inflight: 0,
120
101
  pending: [],
@@ -125,40 +106,36 @@
125
106
  burstDeadline: 0,
126
107
  burstCount: 0,
127
108
  lastBurstTs: 0,
128
- firstShown: false,
129
109
  };
130
110
 
131
- const isBlocked = () => now() < state.blockedUntil;
111
+ const isBlocked = () => now() < S.blockedUntil;
132
112
 
133
113
  function mutate(fn) {
134
- state.mutGuard++;
135
- try { fn(); } finally { state.mutGuard--; }
114
+ S.mutGuard++;
115
+ try { fn(); } finally { S.mutGuard--; }
136
116
  }
137
117
 
138
118
  // ── Config ─────────────────────────────────────────────────────────────────
139
119
 
140
120
  async function fetchConfig() {
141
- if (state.cfg) return state.cfg;
121
+ if (S.cfg) return S.cfg;
142
122
  try {
143
123
  const inline = window.__nbbEzoicCfg;
144
- if (inline && typeof inline === 'object') {
145
- state.cfg = inline;
146
- return state.cfg;
147
- }
124
+ if (inline && typeof inline === 'object') { S.cfg = inline; return S.cfg; }
148
125
  } catch (_) {}
149
126
  try {
150
127
  const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
151
- if (r.ok) state.cfg = await r.json();
128
+ if (r.ok) S.cfg = await r.json();
152
129
  } catch (_) {}
153
- return state.cfg;
130
+ return S.cfg;
154
131
  }
155
132
 
156
133
  function initPools(cfg) {
157
- if (state.poolsReady) return;
158
- state.pools.topics = parseIds(cfg.placeholderIds);
159
- state.pools.posts = parseIds(cfg.messagePlaceholderIds);
160
- state.pools.categories = parseIds(cfg.categoryPlaceholderIds);
161
- state.poolsReady = true;
134
+ if (S.poolsReady) return;
135
+ S.pools.topics = parseIds(cfg.placeholderIds);
136
+ S.pools.posts = parseIds(cfg.messagePlaceholderIds);
137
+ S.pools.categories = parseIds(cfg.categoryPlaceholderIds);
138
+ S.poolsReady = true;
162
139
  }
163
140
 
164
141
  // ── Page identity ──────────────────────────────────────────────────────────
@@ -184,7 +161,7 @@
184
161
  }
185
162
 
186
163
  function getKind() {
187
- return state.kind || (state.kind = detectKind());
164
+ return S.kind || (S.kind = detectKind());
188
165
  }
189
166
 
190
167
  // ── DOM queries ────────────────────────────────────────────────────────────
@@ -204,8 +181,8 @@
204
181
  return out;
205
182
  }
206
183
 
207
- function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
208
- function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
184
+ const getTopics = () => Array.from(document.querySelectorAll(SEL.topic));
185
+ const getCategories = () => Array.from(document.querySelectorAll(SEL.category));
209
186
 
210
187
  // ── Anchor keys & wrap registry ────────────────────────────────────────────
211
188
 
@@ -226,60 +203,45 @@
226
203
  const anchorKey = (klass, el) => `${klass}:${stableId(klass, el)}`;
227
204
 
228
205
  function findWrap(key) {
229
- const w = state.wrapByKey.get(key);
206
+ const w = S.wrapByKey.get(key);
230
207
  return w?.isConnected ? w : null;
231
208
  }
232
209
 
233
210
  function getWrapSet(klass) {
234
- let set = state.wrapsByClass.get(klass);
235
- if (!set) { set = new Set(); state.wrapsByClass.set(klass, set); }
211
+ let set = S.wrapsByClass.get(klass);
212
+ if (!set) { set = new Set(); S.wrapsByClass.set(klass, set); }
236
213
  return set;
237
214
  }
238
215
 
239
- // ── GC disconnected wraps (NodeBB virtualization) ──────────────────────────
216
+ // ── GC disconnected wraps ──────────────────────────────────────────────────
217
+ // NodeBB virtualizes posts off-viewport. The MutationObserver catches most
218
+ // removals, but this is a safety net for edge cases.
240
219
 
241
220
  function gcDisconnectedWraps() {
242
- for (const [key, w] of Array.from(state.wrapByKey.entries())) {
243
- if (!w?.isConnected) state.wrapByKey.delete(key);
221
+ for (const [key, w] of S.wrapByKey) {
222
+ if (!w?.isConnected) S.wrapByKey.delete(key);
244
223
  }
245
- for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
246
- for (const w of Array.from(set)) {
224
+ for (const [klass, set] of S.wrapsByClass) {
225
+ for (const w of set) {
247
226
  if (w?.isConnected) continue;
248
227
  set.delete(w);
249
- try {
250
- const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
251
- if (Number.isFinite(id)) {
252
- state.mountedIds.delete(id);
253
- state.phState.delete(id);
254
- state.lastShow.delete(id);
255
- }
256
- } catch (_) {}
257
- }
258
- if (!set.size) state.wrapsByClass.delete(klass);
259
- }
260
- try {
261
- const live = new Set();
262
- for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
263
- const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
264
- if (id > 0) live.add(id);
265
- }
266
- for (const id of Array.from(state.mountedIds)) {
267
- if (!live.has(id)) {
268
- state.mountedIds.delete(id);
269
- state.phState.delete(id);
270
- state.lastShow.delete(id);
228
+ const id = parseInt(w.getAttribute?.(ATTR.WRAPID), 10);
229
+ if (Number.isFinite(id)) {
230
+ S.mountedIds.delete(id);
231
+ S.lastShow.delete(id);
271
232
  }
272
233
  }
273
- } catch (_) {}
234
+ if (!set.size) S.wrapsByClass.delete(klass);
235
+ }
274
236
  }
275
237
 
276
- // ── Wrap lifecycle detection ───────────────────────────────────────────────
238
+ // ── Wrap lifecycle ─────────────────────────────────────────────────────────
277
239
 
278
240
  function wrapIsLive(wrap) {
279
241
  if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
280
242
  const key = wrap.getAttribute(ATTR.ANCHOR);
281
243
  if (!key) return false;
282
- if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
244
+ if (S.wrapByKey.get(key) === wrap) return wrap.isConnected;
283
245
  const colonIdx = key.indexOf(':');
284
246
  const klass = key.slice(0, colonIdx);
285
247
  const anchorId = key.slice(colonIdx + 1);
@@ -310,14 +272,12 @@
310
272
  const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
311
273
  if (!ph || !isFilled(ph)) return false;
312
274
  wrap.classList.remove('is-empty');
313
- const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
314
- if (id > 0) state.phState.set(id, 'shown');
315
275
  return true;
316
276
  }
317
277
 
318
278
  function scheduleUncollapseChecks(wrap) {
319
279
  if (!wrap) return;
320
- for (const ms of [500, 1500, 3000, 7000, 15000]) {
280
+ for (const ms of [500, 3000, 10000]) {
321
281
  setTimeout(() => { try { clearEmptyIfFilled(wrap); } catch (_) {} }, ms);
322
282
  }
323
283
  }
@@ -325,21 +285,19 @@
325
285
  // ── Pool management ────────────────────────────────────────────────────────
326
286
 
327
287
  function pickId(poolKey) {
328
- const pool = state.pools[poolKey];
288
+ const pool = S.pools[poolKey];
329
289
  if (!pool.length) return null;
330
290
  for (let t = 0; t < pool.length; t++) {
331
- const idx = state.cursors[poolKey] % pool.length;
332
- state.cursors[poolKey] = (state.cursors[poolKey] + 1) % pool.length;
291
+ const idx = S.cursors[poolKey] % pool.length;
292
+ S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
333
293
  const id = pool[idx];
334
- if (!state.mountedIds.has(id)) return id;
294
+ if (!S.mountedIds.has(id)) return id;
335
295
  }
336
296
  return null;
337
297
  }
338
298
 
339
299
  // ── Recycling ──────────────────────────────────────────────────────────────
340
- //
341
- // Per Ezoic docs: destroyPlaceholders(id) → remove old HTML →
342
- // recreate fresh placeholder → showAds(id).
300
+ // Per Ezoic docs: destroyPlaceholders(id) → remove HTML → fresh placeholder → showAds(id)
343
301
 
344
302
  function recycleWrap(klass, targetEl, newKey) {
345
303
  const ez = window.ezstandalone;
@@ -352,18 +310,13 @@
352
310
  let bestEmpty = null, bestEmptyY = Infinity;
353
311
  let bestFull = null, bestFullY = Infinity;
354
312
 
355
- const wraps = state.wrapsByClass.get(klass);
313
+ const wraps = S.wrapsByClass.get(klass);
356
314
  if (!wraps) return null;
357
315
 
358
316
  for (const wrap of wraps) {
359
317
  try {
360
- // Skip young wraps (ad might still be loading)
361
318
  const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
362
- if (t - created < RECYCLE_MIN_AGE_MS) continue;
363
- // Skip wraps with inflight showAds
364
- const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
365
- if (wid > 0 && state.phState.get(wid) === 'show-queued') continue;
366
-
319
+ if (t - created < TIMING.RECYCLE_MIN_AGE_MS) continue;
367
320
  const bottom = wrap.getBoundingClientRect().bottom;
368
321
  if (bottom > threshold) continue;
369
322
  if (!isFilled(wrap)) {
@@ -379,22 +332,19 @@
379
332
 
380
333
  const id = parseInt(best.getAttribute(ATTR.WRAPID), 10);
381
334
  if (!Number.isFinite(id)) return null;
382
-
383
335
  const oldKey = best.getAttribute(ATTR.ANCHOR);
384
336
 
385
337
  // Unobserve before moving
386
338
  try {
387
339
  const ph = best.querySelector(`#${PH_PREFIX}${id}`);
388
- if (ph) state.io?.unobserve(ph);
340
+ if (ph) S.io?.unobserve(ph);
389
341
  } catch (_) {}
390
342
 
391
- // Ezoic recycle: destroy → new DOM → showAds
343
+ // Ezoic recycle: destroy → fresh DOM → showAds
392
344
  const doRecycle = () => {
393
- state.phState.set(id, 'destroyed');
394
345
  try { ez.destroyPlaceholders(id); } catch (_) {}
395
346
 
396
347
  setTimeout(() => {
397
- // Recreate fresh placeholder DOM at new position
398
348
  mutate(() => {
399
349
  best.setAttribute(ATTR.ANCHOR, newKey);
400
350
  best.setAttribute(ATTR.CREATED, String(now()));
@@ -405,27 +355,21 @@
405
355
  const fresh = document.createElement('div');
406
356
  fresh.id = `${PH_PREFIX}${id}`;
407
357
  fresh.setAttribute('data-ezoic-id', String(id));
408
- fresh.style.minHeight = '1px';
409
358
  best.appendChild(fresh);
410
359
  targetEl.insertAdjacentElement('afterend', best);
411
360
  });
412
361
 
413
- if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
414
- state.wrapByKey.set(newKey, best);
362
+ if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
363
+ S.wrapByKey.set(newKey, best);
415
364
 
416
- // Re-show after DOM is settled
417
365
  setTimeout(() => {
418
- observePlaceholder(id);
419
- state.phState.set(id, 'new');
366
+ observePh(id);
420
367
  enqueueShow(id);
421
368
  }, TIMING.RECYCLE_DELAY_MS);
422
369
  }, TIMING.RECYCLE_DELAY_MS);
423
370
  };
424
371
 
425
- try {
426
- (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle();
427
- } catch (_) {}
428
-
372
+ try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doRecycle) : doRecycle(); } catch (_) {}
429
373
  return { id, wrap: best };
430
374
  }
431
375
 
@@ -439,11 +383,9 @@
439
383
  w.setAttribute(ATTR.CREATED, String(now()));
440
384
  w.setAttribute(ATTR.SHOWN, '0');
441
385
  w.style.cssText = 'width:100%;display:block';
442
-
443
386
  const ph = document.createElement('div');
444
387
  ph.id = `${PH_PREFIX}${id}`;
445
388
  ph.setAttribute('data-ezoic-id', String(id));
446
- ph.style.minHeight = '1px';
447
389
  w.appendChild(ph);
448
390
  return w;
449
391
  }
@@ -451,15 +393,12 @@
451
393
  function insertAfter(el, id, klass, key) {
452
394
  if (!el?.insertAdjacentElement) return null;
453
395
  if (findWrap(key)) return null;
454
- if (state.mountedIds.has(id)) return null;
455
- const existing = document.getElementById(`${PH_PREFIX}${id}`);
456
- if (existing?.isConnected) return null;
457
-
396
+ if (S.mountedIds.has(id)) return null;
397
+ if (document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) return null;
458
398
  const w = makeWrap(id, klass, key);
459
399
  mutate(() => el.insertAdjacentElement('afterend', w));
460
- state.mountedIds.add(id);
461
- state.phState.set(id, 'new');
462
- state.wrapByKey.set(key, w);
400
+ S.mountedIds.add(id);
401
+ S.wrapByKey.set(key, w);
463
402
  getWrapSet(klass).add(w);
464
403
  return w;
465
404
  }
@@ -467,17 +406,17 @@
467
406
  function dropWrap(w) {
468
407
  try {
469
408
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
470
- if (ph instanceof Element) state.io?.unobserve(ph);
409
+ if (ph instanceof Element) S.io?.unobserve(ph);
471
410
  const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
472
411
  if (Number.isFinite(id)) {
473
- state.mountedIds.delete(id);
474
- state.phState.delete(id);
412
+ S.mountedIds.delete(id);
413
+ S.lastShow.delete(id);
475
414
  }
476
415
  const key = w.getAttribute(ATTR.ANCHOR);
477
- if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
416
+ if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
478
417
  for (const cls of w.classList) {
479
418
  if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
480
- state.wrapsByClass.get(cls)?.delete(w);
419
+ S.wrapsByClass.get(cls)?.delete(w);
481
420
  break;
482
421
  }
483
422
  }
@@ -490,7 +429,7 @@
490
429
  function pruneOrphansBetween() {
491
430
  const klass = 'ezoic-ad-between';
492
431
  const cfg = KIND[klass];
493
- const wraps = state.wrapsByClass.get(klass);
432
+ const wraps = S.wrapsByClass.get(klass);
494
433
  if (!wraps?.size) return;
495
434
 
496
435
  const liveAnchors = new Set();
@@ -498,16 +437,13 @@
498
437
  const v = el.getAttribute(cfg.anchorAttr);
499
438
  if (v) liveAnchors.add(v);
500
439
  }
501
-
502
440
  const t = now();
503
441
  for (const w of wraps) {
504
442
  const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
505
443
  if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
506
444
  const key = w.getAttribute(ATTR.ANCHOR) ?? '';
507
445
  const sid = key.slice(klass.length + 1);
508
- if (!sid || !liveAnchors.has(sid)) {
509
- mutate(() => dropWrap(w));
510
- }
446
+ if (!sid || !liveAnchors.has(sid)) mutate(() => dropWrap(w));
511
447
  }
512
448
  }
513
449
 
@@ -531,26 +467,18 @@
531
467
  function injectBetween(klass, items, interval, showFirst, poolKey) {
532
468
  if (!items.length) return 0;
533
469
  let inserted = 0;
534
-
535
470
  for (const el of items) {
536
- if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
471
+ if (inserted >= MAX_INSERTS_RUN) break;
537
472
  if (!el?.isConnected) continue;
538
-
539
473
  const ord = ordinal(klass, el);
540
474
  if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
541
475
  if (adjacentWrap(el)) continue;
542
-
543
476
  const key = anchorKey(klass, el);
544
477
  if (findWrap(key)) continue;
545
-
546
478
  const id = pickId(poolKey);
547
479
  if (id) {
548
480
  const w = insertAfter(el, id, klass, key);
549
- if (w) {
550
- observePlaceholder(id);
551
- if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
552
- inserted++;
553
- }
481
+ if (w) { observePh(id); inserted++; }
554
482
  } else {
555
483
  const recycled = recycleWrap(klass, el, key);
556
484
  if (!recycled) break;
@@ -563,69 +491,58 @@
563
491
  // ── IntersectionObserver ───────────────────────────────────────────────────
564
492
 
565
493
  function getIO() {
566
- if (state.io) return state.io;
494
+ if (S.io) return S.io;
567
495
  try {
568
- state.io = new IntersectionObserver(entries => {
569
- for (const entry of entries) {
570
- if (!entry.isIntersecting) continue;
571
- if (entry.target instanceof Element) state.io?.unobserve(entry.target);
572
- const id = parseInt(entry.target.getAttribute('data-ezoic-id'), 10);
496
+ S.io = new IntersectionObserver(entries => {
497
+ for (const e of entries) {
498
+ if (!e.isIntersecting) continue;
499
+ if (e.target instanceof Element) S.io?.unobserve(e.target);
500
+ const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
573
501
  if (id > 0) enqueueShow(id);
574
502
  }
575
503
  }, {
576
504
  root: null,
577
- rootMargin: isMobile() ? IO_MARGIN.MOBILE : IO_MARGIN.DESKTOP,
505
+ rootMargin: isMobile() ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP,
578
506
  threshold: 0,
579
507
  });
580
- } catch (_) { state.io = null; }
581
- return state.io;
508
+ } catch (_) { S.io = null; }
509
+ return S.io;
582
510
  }
583
511
 
584
- function observePlaceholder(id) {
512
+ function observePh(id) {
585
513
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
586
- if (ph?.isConnected) {
587
- try { getIO()?.observe(ph); } catch (_) {}
588
- }
514
+ if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
589
515
  }
590
516
 
591
517
  // ── Show queue ─────────────────────────────────────────────────────────────
592
518
 
593
519
  function enqueueShow(id) {
594
520
  if (!id || isBlocked()) return;
595
- const st = state.phState.get(id);
596
- if (st === 'show-queued' || st === 'shown') return;
597
- if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
598
-
599
- if (state.inflight >= LIMITS.MAX_INFLIGHT) {
600
- if (!state.pendingSet.has(id)) {
601
- state.pending.push(id);
602
- state.pendingSet.add(id);
603
- state.phState.set(id, 'show-queued');
604
- }
521
+ if (now() - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
522
+ if (S.inflight >= MAX_INFLIGHT) {
523
+ if (!S.pendingSet.has(id)) { S.pending.push(id); S.pendingSet.add(id); }
605
524
  return;
606
525
  }
607
- state.phState.set(id, 'show-queued');
608
526
  startShow(id);
609
527
  }
610
528
 
611
529
  function drainQueue() {
612
530
  if (isBlocked()) return;
613
- while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
614
- const id = state.pending.shift();
615
- state.pendingSet.delete(id);
531
+ while (S.inflight < MAX_INFLIGHT && S.pending.length) {
532
+ const id = S.pending.shift();
533
+ S.pendingSet.delete(id);
616
534
  startShow(id);
617
535
  }
618
536
  }
619
537
 
620
538
  function startShow(id) {
621
539
  if (!id || isBlocked()) return;
622
- state.inflight++;
623
-
624
- let released = false;
540
+ S.inflight++;
541
+ let done = false;
625
542
  const release = () => {
626
- if (released) return;
627
- released = true;
628
- state.inflight = Math.max(0, state.inflight - 1);
543
+ if (done) return;
544
+ done = true;
545
+ S.inflight = Math.max(0, S.inflight - 1);
629
546
  drainQueue();
630
547
  };
631
548
  const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
@@ -633,52 +550,31 @@
633
550
  requestAnimationFrame(() => {
634
551
  try {
635
552
  if (isBlocked()) { clearTimeout(timer); return release(); }
636
-
637
553
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
638
- if (!ph?.isConnected) {
639
- state.phState.delete(id);
640
- clearTimeout(timer);
641
- return release();
642
- }
643
-
644
- if (isFilled(ph) || isPlaceholderUsed(ph)) {
645
- state.phState.set(id, 'shown');
646
- clearTimeout(timer);
647
- return release();
648
- }
554
+ if (!ph?.isConnected || isFilled(ph)) { clearTimeout(timer); return release(); }
649
555
 
650
556
  const t = now();
651
- if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
652
- clearTimeout(timer);
653
- return release();
654
- }
655
- state.lastShow.set(id, t);
557
+ if (t - (S.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
558
+ S.lastShow.set(id, t);
656
559
 
657
- try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
658
- state.phState.set(id, 'shown');
560
+ const wrap = ph.closest(`.${WRAP_CLASS}`);
561
+ try { wrap?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
659
562
 
660
563
  window.ezstandalone = window.ezstandalone || {};
661
564
  const ez = window.ezstandalone;
662
-
663
565
  const doShow = () => {
664
- const wrap = ph.closest(`.${WRAP_CLASS}`);
665
566
  try { ez.showAds(id); } catch (_) {}
666
567
  if (wrap) scheduleUncollapseChecks(wrap);
667
568
  scheduleEmptyCheck(id, t);
668
569
  setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
669
570
  };
670
-
671
571
  Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
672
- } catch (_) {
673
- clearTimeout(timer);
674
- release();
675
- }
572
+ } catch (_) { clearTimeout(timer); release(); }
676
573
  });
677
574
  }
678
575
 
679
576
  function scheduleEmptyCheck(id, showTs) {
680
- // Two-pass check: conservative to avoid collapsing slow-loading ads
681
- for (const delay of [TIMING.EMPTY_CHECK_EARLY_MS, TIMING.EMPTY_CHECK_LATE_MS]) {
577
+ for (const delay of [TIMING.EMPTY_CHECK_MS_1, TIMING.EMPTY_CHECK_MS_2]) {
682
578
  setTimeout(() => {
683
579
  try {
684
580
  const ph = document.getElementById(`${PH_PREFIX}${id}`);
@@ -686,9 +582,8 @@
686
582
  if (!wrap || !ph?.isConnected) return;
687
583
  if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
688
584
  if (clearEmptyIfFilled(wrap)) return;
689
- // Don't collapse if a GPT slot exists (might still be loading)
585
+ // Don't collapse if GPT slot exists (still loading)
690
586
  if (ph.querySelector('[id^="div-gpt-ad"]')) return;
691
- // Don't collapse if placeholder has meaningful height
692
587
  if (ph.offsetHeight > 10) return;
693
588
  wrap.classList.add('is-empty');
694
589
  } catch (_) {}
@@ -697,10 +592,7 @@
697
592
  }
698
593
 
699
594
  // ── Patch Ezoic showAds ────────────────────────────────────────────────────
700
- //
701
- // Intercepts ez.showAds() to batch calls and filter disconnected placeholders.
702
- // IMPORTANT: no-arg showAds() calls (used by Ezoic for page transitions)
703
- // are passed through unmodified.
595
+ // Matches v50: individual calls, no batching, pass-through no-arg calls.
704
596
 
705
597
  function patchShowAds() {
706
598
  const apply = () => {
@@ -709,53 +601,22 @@
709
601
  const ez = window.ezstandalone;
710
602
  if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
711
603
  window.__nbbEzPatched = true;
712
-
713
604
  const orig = ez.showAds.bind(ez);
714
- const queue = new Set();
715
- let flushTimer = null;
716
-
717
- const flush = () => {
718
- flushTimer = null;
719
- if (isBlocked() || !queue.size) return;
720
- const ids = Array.from(queue).sort((a, b) => a - b);
721
- queue.clear();
722
- const valid = ids.filter(id => {
723
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
724
- if (!ph?.isConnected) { state.phState.delete(id); return false; }
725
- if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
726
- return true;
727
- });
728
- for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
729
- const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
730
- try { orig(...chunk); } catch (_) {
731
- for (const cid of chunk) { try { orig(cid); } catch (_) {} }
732
- }
733
- }
734
- };
735
-
736
605
  ez.showAds = function (...args) {
737
- // No-arg call = Ezoic page refresh — pass through unmodified
738
- if (args.length === 0) {
739
- return orig();
740
- }
606
+ if (args.length === 0) return orig();
741
607
  if (isBlocked()) return;
742
608
  const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
609
+ const seen = new Set();
743
610
  for (const v of ids) {
744
611
  const id = parseInt(v, 10);
745
- if (!Number.isFinite(id) || id <= 0) continue;
746
- const ph = document.getElementById(`${PH_PREFIX}${id}`);
747
- if (!ph?.isConnected) continue;
748
- if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); continue; }
749
- state.phState.set(id, 'show-queued');
750
- queue.add(id);
751
- }
752
- if (queue.size && !flushTimer) {
753
- flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
612
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
613
+ if (!document.getElementById(`${PH_PREFIX}${id}`)?.isConnected) continue;
614
+ seen.add(id);
615
+ try { orig(id); } catch (_) {}
754
616
  }
755
617
  };
756
618
  } catch (_) {}
757
619
  };
758
-
759
620
  apply();
760
621
  if (!window.__nbbEzPatched) {
761
622
  window.ezstandalone = window.ezstandalone || {};
@@ -767,6 +628,7 @@
767
628
 
768
629
  async function runCore() {
769
630
  if (isBlocked()) return 0;
631
+ patchShowAds();
770
632
  try { gcDisconnectedWraps(); } catch (_) {}
771
633
 
772
634
  const cfg = await fetchConfig();
@@ -783,32 +645,26 @@
783
645
  };
784
646
 
785
647
  if (kind === 'topic') {
786
- return exec(
787
- 'ezoic-ad-message', getPosts,
788
- cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
789
- );
648
+ return exec('ezoic-ad-message', getPosts,
649
+ cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts');
790
650
  }
791
651
  if (kind === 'categoryTopics') {
792
652
  pruneOrphansBetween();
793
- return exec(
794
- 'ezoic-ad-between', getTopics,
795
- cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
796
- );
653
+ return exec('ezoic-ad-between', getTopics,
654
+ cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics');
797
655
  }
798
- return exec(
799
- 'ezoic-ad-categories', getCategories,
800
- cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
801
- );
656
+ return exec('ezoic-ad-categories', getCategories,
657
+ cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories');
802
658
  }
803
659
 
804
660
  // ── Scheduler & burst ──────────────────────────────────────────────────────
805
661
 
806
662
  function scheduleRun(cb) {
807
- if (state.runQueued) return;
808
- state.runQueued = true;
663
+ if (S.runQueued) return;
664
+ S.runQueued = true;
809
665
  requestAnimationFrame(async () => {
810
- state.runQueued = false;
811
- if (state.pageKey && pageKey() !== state.pageKey) return;
666
+ S.runQueued = false;
667
+ if (S.pageKey && pageKey() !== S.pageKey) return;
812
668
  let n = 0;
813
669
  try { n = await runCore(); } catch (_) {}
814
670
  try { cb?.(n); } catch (_) {}
@@ -818,21 +674,20 @@
818
674
  function requestBurst() {
819
675
  if (isBlocked()) return;
820
676
  const t = now();
821
- if (t - state.lastBurstTs < TIMING.BURST_COOLDOWN_MS) return;
822
- state.lastBurstTs = t;
823
- state.pageKey = pageKey();
824
- state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
825
- if (state.burstActive) return;
826
- state.burstActive = true;
827
- state.burstCount = 0;
677
+ if (t - S.lastBurstTs < TIMING.BURST_COOLDOWN_MS) return;
678
+ S.lastBurstTs = t;
679
+ S.pageKey = pageKey();
680
+ S.burstDeadline = t + BURST_WINDOW_MS;
681
+ if (S.burstActive) return;
682
+ S.burstActive = true;
683
+ S.burstCount = 0;
828
684
  const step = () => {
829
- if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
830
- state.burstActive = false;
831
- return;
685
+ if (pageKey() !== S.pageKey || isBlocked() || now() > S.burstDeadline || S.burstCount >= MAX_BURST_STEPS) {
686
+ S.burstActive = false; return;
832
687
  }
833
- state.burstCount++;
688
+ S.burstCount++;
834
689
  scheduleRun(n => {
835
- if (!n && !state.pending.length) { state.burstActive = false; return; }
690
+ if (!n && !S.pending.length) { S.burstActive = false; return; }
836
691
  setTimeout(step, n > 0 ? 150 : 300);
837
692
  });
838
693
  };
@@ -842,46 +697,32 @@
842
697
  // ── Cleanup on navigation ──────────────────────────────────────────────────
843
698
 
844
699
  function cleanup() {
845
- state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
846
-
847
- // Tell Ezoic to destroy all placeholders BEFORE we remove DOM elements.
848
- // This prevents GPT slotDestroyed events and Ezoic 400 errors.
849
- try {
850
- const ez = window.ezstandalone;
851
- if (typeof ez?.destroyAll === 'function') {
852
- ez.destroyAll();
853
- }
854
- } catch (_) {}
855
-
700
+ S.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
856
701
  mutate(() => {
857
702
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) dropWrap(w);
858
703
  });
859
- state.cfg = null;
860
- state.poolsReady = false;
861
- state.pools = { topics: [], posts: [], categories: [] };
862
- state.cursors = { topics: 0, posts: 0, categories: 0 };
863
- state.mountedIds.clear();
864
- state.lastShow.clear();
865
- state.wrapByKey.clear();
866
- state.wrapsByClass.clear();
867
- state.kind = null;
868
- state.phState.clear();
869
- state.inflight = 0;
870
- state.pending = [];
871
- state.pendingSet.clear();
872
- state.burstActive = false;
873
- state.runQueued = false;
874
- state.firstShown = false;
704
+ S.cfg = null;
705
+ S.poolsReady = false;
706
+ S.pools = { topics: [], posts: [], categories: [] };
707
+ S.cursors = { topics: 0, posts: 0, categories: 0 };
708
+ S.mountedIds.clear();
709
+ S.lastShow.clear();
710
+ S.wrapByKey.clear();
711
+ S.wrapsByClass.clear();
712
+ S.kind = null;
713
+ S.inflight = 0;
714
+ S.pending = [];
715
+ S.pendingSet.clear();
716
+ S.burstActive = false;
717
+ S.runQueued = false;
875
718
  }
876
719
 
877
720
  // ── MutationObserver ───────────────────────────────────────────────────────
878
721
 
879
722
  function ensureDomObserver() {
880
- if (state.domObs) return;
881
-
882
- state.domObs = new MutationObserver(muts => {
883
- if (state.mutGuard > 0 || isBlocked()) return;
884
-
723
+ if (S.domObs) return;
724
+ S.domObs = new MutationObserver(muts => {
725
+ if (S.mutGuard > 0 || isBlocked()) return;
885
726
  let needsBurst = false;
886
727
  const kind = getKind();
887
728
  const relevantSels =
@@ -889,10 +730,8 @@
889
730
  kind === 'categoryTopics' ? [SEL.topic] :
890
731
  kind === 'categories' ? [SEL.category] :
891
732
  [SEL.post, SEL.topic, SEL.category];
892
-
893
733
  for (const m of muts) {
894
734
  if (m.type !== 'childList') continue;
895
-
896
735
  // Free IDs from wraps removed by NodeBB virtualization
897
736
  for (const node of m.removedNodes) {
898
737
  if (!(node instanceof Element)) continue;
@@ -901,15 +740,13 @@
901
740
  dropWrap(node);
902
741
  } else {
903
742
  const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
904
- if (wraps?.length) { for (const w of wraps) dropWrap(w); }
743
+ if (wraps?.length) for (const w of wraps) dropWrap(w);
905
744
  }
906
745
  } catch (_) {}
907
746
  }
908
-
909
747
  for (const node of m.addedNodes) {
910
748
  if (!(node instanceof Element)) continue;
911
-
912
- // Ad fill detection
749
+ // Ad fill detection → uncollapse
913
750
  try {
914
751
  if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
915
752
  const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
@@ -917,20 +754,19 @@
917
754
  if (wrap) clearEmptyIfFilled(wrap);
918
755
  }
919
756
  } catch (_) {}
920
-
921
757
  // Re-observe wraps re-inserted by NodeBB virtualization
922
758
  try {
923
- const wraps = node.classList?.contains(WRAP_CLASS)
759
+ const reinserted = node.classList?.contains(WRAP_CLASS)
924
760
  ? [node]
925
761
  : Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
926
- for (const wrap of wraps) {
762
+ for (const wrap of reinserted) {
927
763
  const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
928
- if (!id || id <= 0) continue;
929
- const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
930
- if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
764
+ if (id > 0) {
765
+ const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
766
+ if (ph) try { getIO()?.observe(ph); } catch (_) {}
767
+ }
931
768
  }
932
769
  } catch (_) {}
933
-
934
770
  // New content detection
935
771
  if (!needsBurst) {
936
772
  for (const sel of relevantSels) {
@@ -942,29 +778,23 @@
942
778
  }
943
779
  if (needsBurst) break;
944
780
  }
945
-
946
781
  if (needsBurst) requestBurst();
947
782
  });
948
-
949
- try {
950
- state.domObs.observe(document.body, { childList: true, subtree: true });
951
- } catch (_) {}
783
+ try { S.domObs.observe(document.body, { childList: true, subtree: true }); } catch (_) {}
952
784
  }
953
785
 
954
- // ── TCF / CMP Protection (3 layers) ────────────────────────────────────────
786
+ // ── TCF / CMP Protection ───────────────────────────────────────────────────
955
787
 
956
788
  function ensureTcfLocator() {
957
789
  if (!window.__tcfapi && !window.__cmp) return;
958
-
959
790
  const LOCATOR_ID = '__tcfapiLocator';
960
-
961
791
  const ensureInHead = () => {
962
792
  let existing = document.getElementById(LOCATOR_ID);
963
793
  if (existing) {
964
794
  if (existing.parentElement !== document.head) {
965
795
  try { document.head.appendChild(existing); } catch (_) {}
966
796
  }
967
- return existing;
797
+ return;
968
798
  }
969
799
  const f = document.createElement('iframe');
970
800
  f.style.display = 'none';
@@ -972,9 +802,7 @@
972
802
  try { document.head.appendChild(f); } catch (_) {
973
803
  (document.body || document.documentElement).appendChild(f);
974
804
  }
975
- return f;
976
805
  };
977
-
978
806
  ensureInHead();
979
807
 
980
808
  if (!window.__nbbCmpGuarded) {
@@ -1016,12 +844,8 @@
1016
844
  window.__nbbTcfObs.observe(document.body || document.documentElement, {
1017
845
  childList: true, subtree: false,
1018
846
  });
1019
- } catch (_) {}
1020
- try {
1021
847
  if (document.head) {
1022
- window.__nbbTcfObs.observe(document.head, {
1023
- childList: true, subtree: false,
1024
- });
848
+ window.__nbbTcfObs.observe(document.head, { childList: true, subtree: false });
1025
849
  }
1026
850
  } catch (_) {}
1027
851
  }
@@ -1042,8 +866,7 @@
1042
866
  window.__nbbAriaObs = new MutationObserver(remove);
1043
867
  try {
1044
868
  window.__nbbAriaObs.observe(document.body, {
1045
- attributes: true,
1046
- attributeFilter: ['aria-hidden'],
869
+ attributes: true, attributeFilter: ['aria-hidden'],
1047
870
  });
1048
871
  } catch (_) {}
1049
872
  }
@@ -1053,8 +876,7 @@
1053
876
  function muteConsole() {
1054
877
  if (window.__nbbEzMuted) return;
1055
878
  window.__nbbEzMuted = true;
1056
-
1057
- const PREFIXES = [
879
+ const MUTED = [
1058
880
  '[EzoicAds JS]: Placeholder Id',
1059
881
  'No valid placeholders for loadMore',
1060
882
  'cannot call refresh on the same page',
@@ -1063,22 +885,22 @@
1063
885
  '[CMP] Error in custom getTCData',
1064
886
  'vignette: no interstitial API',
1065
887
  'Ezoic JS-Enable should only ever',
1066
- ];
1067
- const PATTERNS = [
888
+ '### sending slotDestroyed',
889
+ 'Error loading identity bridging',
1068
890
  `with id ${PH_PREFIX}`,
1069
891
  'adsbygoogle.push() error: All',
1070
892
  'has already been defined',
1071
893
  'bad response. Status',
894
+ 'slotDestroyed event',
895
+ 'identity bridging',
1072
896
  ];
1073
-
1074
897
  for (const method of ['log', 'info', 'warn', 'error']) {
1075
898
  const orig = console[method];
1076
899
  if (typeof orig !== 'function') continue;
1077
900
  console[method] = function (...args) {
1078
901
  if (typeof args[0] === 'string') {
1079
902
  const msg = args[0];
1080
- for (const p of PREFIXES) { if (msg.startsWith(p)) return; }
1081
- for (const p of PATTERNS) { if (msg.includes(p)) return; }
903
+ for (const p of MUTED) { if (msg.includes(p)) return; }
1082
904
  }
1083
905
  return orig.apply(console, args);
1084
906
  };
@@ -1087,28 +909,25 @@
1087
909
 
1088
910
  // ── Network warmup ─────────────────────────────────────────────────────────
1089
911
 
1090
- let _networkWarmed = false;
1091
-
912
+ const _warmed = new Set();
1092
913
  function warmNetwork() {
1093
- if (_networkWarmed) return;
1094
- _networkWarmed = true;
1095
914
  const head = document.head;
1096
915
  if (!head) return;
1097
- const hints = [
916
+ for (const [rel, href, cors] of [
1098
917
  ['preconnect', 'https://g.ezoic.net', true ],
1099
918
  ['preconnect', 'https://go.ezoic.net', true ],
1100
919
  ['preconnect', 'https://securepubads.g.doubleclick.net', true ],
1101
920
  ['preconnect', 'https://pagead2.googlesyndication.com', true ],
1102
921
  ['dns-prefetch', 'https://g.ezoic.net', false],
1103
922
  ['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
1104
- ];
1105
- for (const [rel, href, cors] of hints) {
1106
- if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
1107
- const link = document.createElement('link');
1108
- link.rel = rel;
1109
- link.href = href;
1110
- if (cors) link.crossOrigin = 'anonymous';
1111
- head.appendChild(link);
923
+ ]) {
924
+ const k = `${rel}|${href}`;
925
+ if (_warmed.has(k)) continue;
926
+ _warmed.add(k);
927
+ const l = document.createElement('link');
928
+ l.rel = rel; l.href = href;
929
+ if (cors) l.crossOrigin = 'anonymous';
930
+ head.appendChild(l);
1112
931
  }
1113
932
  }
1114
933
 
@@ -1117,54 +936,30 @@
1117
936
  function bindNodeBB() {
1118
937
  const $ = window.jQuery;
1119
938
  if (!$) return;
1120
-
1121
939
  $(window).off('.nbbEzoic');
1122
-
1123
- // Only cleanup on actual page change, not same-page pagination
1124
- $(window).on('action:ajaxify.start.nbbEzoic', (ev, data) => {
1125
- const targetUrl = data?.url || data?.tpl_url || '';
1126
- const currentPath = location.pathname.replace(/^\//, '');
1127
- if (targetUrl && targetUrl.replace(/[?#].*$/, '') === currentPath.replace(/[?#].*$/, '')) {
1128
- return; // Same page — skip cleanup
1129
- }
1130
- cleanup();
1131
- });
1132
-
940
+ $(window).on('action:ajaxify.start.nbbEzoic', cleanup);
1133
941
  $(window).on('action:ajaxify.end.nbbEzoic', () => {
1134
- state.pageKey = pageKey();
1135
- state.kind = null;
1136
- state.blockedUntil = 0;
1137
-
1138
- muteConsole();
1139
- ensureTcfLocator();
1140
- protectAriaHidden();
1141
- warmNetwork();
1142
- patchShowAds();
1143
- getIO();
1144
- ensureDomObserver();
942
+ S.pageKey = pageKey();
943
+ S.kind = null;
944
+ S.blockedUntil = 0;
945
+ muteConsole(); ensureTcfLocator(); protectAriaHidden();
946
+ warmNetwork(); patchShowAds(); getIO(); ensureDomObserver();
1145
947
  requestBurst();
1146
948
  });
1147
949
 
1148
950
  const burstEvents = [
1149
- 'action:ajaxify.contentLoaded',
1150
- 'action:posts.loaded',
1151
- 'action:topics.loaded',
1152
- 'action:categories.loaded',
1153
- 'action:category.loaded',
1154
- 'action:topic.loaded',
951
+ 'action:ajaxify.contentLoaded', 'action:posts.loaded',
952
+ 'action:topics.loaded', 'action:categories.loaded',
953
+ 'action:category.loaded', 'action:topic.loaded',
1155
954
  ].map(e => `${e}.nbbEzoic`).join(' ');
1156
-
1157
955
  $(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
1158
956
 
1159
957
  try {
1160
958
  require(['hooks'], hooks => {
1161
959
  if (typeof hooks?.on !== 'function') return;
1162
960
  for (const ev of [
1163
- 'action:ajaxify.end',
1164
- 'action:posts.loaded',
1165
- 'action:topics.loaded',
1166
- 'action:categories.loaded',
1167
- 'action:topic.loaded',
961
+ 'action:ajaxify.end', 'action:posts.loaded', 'action:topics.loaded',
962
+ 'action:categories.loaded', 'action:topic.loaded',
1168
963
  ]) {
1169
964
  try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
1170
965
  }
@@ -1177,16 +972,13 @@
1177
972
  window.addEventListener('scroll', () => {
1178
973
  if (ticking) return;
1179
974
  ticking = true;
1180
- requestAnimationFrame(() => {
1181
- ticking = false;
1182
- requestBurst();
1183
- });
975
+ requestAnimationFrame(() => { ticking = false; requestBurst(); });
1184
976
  }, { passive: true });
1185
977
  }
1186
978
 
1187
979
  // ── Boot ───────────────────────────────────────────────────────────────────
1188
980
 
1189
- state.pageKey = pageKey();
981
+ S.pageKey = pageKey();
1190
982
  muteConsole();
1191
983
  ensureTcfLocator();
1192
984
  protectAriaHidden();
@@ -1196,7 +988,7 @@
1196
988
  ensureDomObserver();
1197
989
  bindNodeBB();
1198
990
  bindScroll();
1199
- state.blockedUntil = 0;
991
+ S.blockedUntil = 0;
1200
992
  requestBurst();
1201
993
 
1202
994
  })();