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