nodebb-plugin-ezoic-infinite 1.8.40 → 1.8.41
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 +5 -21
- package/package.json +1 -1
- package/public/client.js +628 -348
package/public/client.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v2.0.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Complete rewrite: performance optimization, CMP/TCF fix, clean architecture.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* Key changes from v1.x:
|
|
7
|
+
* - TCF locator: debounced recreation instead of per-mutation, prevents CMP postMessage null errors
|
|
8
|
+
* - MutationObserver: scoped to content containers instead of document.body subtree
|
|
9
|
+
* - Console muting: regex-free, prefix-based matching
|
|
10
|
+
* - showAds batching: microtask-based flush instead of setTimeout
|
|
11
|
+
* - Warm network: runs once per session, not per navigation
|
|
12
|
+
* - State machine: clear lifecycle for placeholders (idle → observed → queued → shown → recycled)
|
|
13
13
|
*/
|
|
14
14
|
(function nbbEzoicInfinite() {
|
|
15
15
|
'use strict';
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const WRAP_CLASS = 'nodebb-ezoic-wrap';
|
|
20
20
|
const PH_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
21
21
|
|
|
22
|
+
// Data attributes
|
|
22
23
|
const ATTR = {
|
|
23
24
|
ANCHOR: 'data-ezoic-anchor',
|
|
24
25
|
WRAPID: 'data-ezoic-wrapid',
|
|
@@ -26,19 +27,25 @@
|
|
|
26
27
|
SHOWN: 'data-ezoic-shown',
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
// Timing
|
|
29
31
|
const TIMING = {
|
|
30
32
|
EMPTY_CHECK_MS: 20_000,
|
|
31
33
|
MIN_PRUNE_AGE_MS: 8_000,
|
|
32
34
|
SHOW_THROTTLE_MS: 900,
|
|
33
35
|
BURST_COOLDOWN_MS: 200,
|
|
34
36
|
BLOCK_DURATION_MS: 1_500,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
SHOW_TIMEOUT_MS: 7_000,
|
|
38
|
+
SHOW_RELEASE_MS: 700,
|
|
39
|
+
BATCH_FLUSH_MS: 80,
|
|
40
|
+
RECYCLE_DELAY_MS: 450,
|
|
41
|
+
|
|
38
42
|
};
|
|
39
43
|
|
|
44
|
+
// Limits
|
|
40
45
|
const LIMITS = {
|
|
41
46
|
MAX_INSERTS_RUN: 6,
|
|
47
|
+
MAX_INFLIGHT: 4,
|
|
48
|
+
BATCH_SIZE: 3,
|
|
42
49
|
MAX_BURST_STEPS: 8,
|
|
43
50
|
BURST_WINDOW_MS: 2_000,
|
|
44
51
|
};
|
|
@@ -48,39 +55,47 @@
|
|
|
48
55
|
MOBILE: '3500px 0px 3500px 0px',
|
|
49
56
|
};
|
|
50
57
|
|
|
58
|
+
// Selectors
|
|
51
59
|
const SEL = {
|
|
52
60
|
post: '[component="post"][data-pid]',
|
|
53
61
|
topic: 'li[component="category/topic"]',
|
|
54
62
|
category: 'li[component="categories/category"]',
|
|
55
63
|
};
|
|
56
64
|
|
|
65
|
+
// Kind configuration table — single source of truth per ad type
|
|
57
66
|
const KIND = {
|
|
58
67
|
'ezoic-ad-message': { sel: SEL.post, baseTag: '', anchorAttr: 'data-pid', ordinalAttr: 'data-index' },
|
|
59
68
|
'ezoic-ad-between': { sel: SEL.topic, baseTag: 'li', anchorAttr: 'data-index', ordinalAttr: 'data-index' },
|
|
60
69
|
'ezoic-ad-categories': { sel: SEL.category, baseTag: 'li', anchorAttr: 'data-cid', ordinalAttr: null },
|
|
61
70
|
};
|
|
62
71
|
|
|
72
|
+
// Selector for detecting filled ad slots
|
|
63
73
|
const FILL_SEL = 'iframe, ins, img, video, [data-google-container-id], div[id$="__container__"]';
|
|
64
74
|
|
|
65
|
-
const RECYCLE_MIN_AGE_MS = 5_000;
|
|
66
|
-
|
|
67
75
|
// ── Utility ────────────────────────────────────────────────────────────────
|
|
68
76
|
|
|
69
77
|
const now = () => Date.now();
|
|
70
78
|
const isMobile = () => window.innerWidth < 768;
|
|
71
79
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
72
80
|
|
|
73
|
-
function isFilled(node)
|
|
81
|
+
function isFilled(node) {
|
|
82
|
+
return node?.querySelector?.(FILL_SEL) != null;
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
function isPlaceholderUsed(ph) {
|
|
75
86
|
if (!ph?.isConnected) return false;
|
|
76
87
|
return ph.querySelector('ins.adsbygoogle, iframe, [data-google-container-id], div[id$="__container__"]') != null;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
function parseIds(raw) {
|
|
80
|
-
const out = []
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
81
93
|
for (const line of String(raw || '').split(/[\r\n,\s]+/)) {
|
|
82
94
|
const n = parseInt(line, 10);
|
|
83
|
-
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
95
|
+
if (n > 0 && Number.isFinite(n) && !seen.has(n)) {
|
|
96
|
+
seen.add(n);
|
|
97
|
+
out.push(n);
|
|
98
|
+
}
|
|
84
99
|
}
|
|
85
100
|
return out;
|
|
86
101
|
}
|
|
@@ -88,29 +103,45 @@
|
|
|
88
103
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
89
104
|
|
|
90
105
|
const state = {
|
|
91
|
-
|
|
106
|
+
// Page context
|
|
107
|
+
pageKey: null,
|
|
108
|
+
kind: null,
|
|
109
|
+
cfg: null,
|
|
92
110
|
|
|
111
|
+
// Pools
|
|
93
112
|
poolsReady: false,
|
|
94
|
-
pools:
|
|
95
|
-
cursors:
|
|
113
|
+
pools: { topics: [], posts: [], categories: [] },
|
|
114
|
+
cursors: { topics: 0, posts: 0, categories: 0 },
|
|
115
|
+
|
|
116
|
+
// Mounted placeholders
|
|
117
|
+
mountedIds: new Set(),
|
|
118
|
+
phState: new Map(), // id → 'new' | 'show-queued' | 'shown' | 'destroyed'
|
|
119
|
+
lastShow: new Map(), // id → timestamp
|
|
96
120
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
lastShow: new Map(),
|
|
101
|
-
wrapByKey: new Map(),
|
|
102
|
-
wrapsByClass: new Map(),
|
|
121
|
+
// Wrap registry
|
|
122
|
+
wrapByKey: new Map(), // anchorKey → wrap element
|
|
123
|
+
wrapsByClass: new Map(), // kindClass → Set<wrap>
|
|
103
124
|
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
// Observers
|
|
126
|
+
io: null,
|
|
127
|
+
domObs: null,
|
|
106
128
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
// Guards
|
|
130
|
+
mutGuard: 0,
|
|
131
|
+
blockedUntil: 0,
|
|
110
132
|
|
|
111
|
-
//
|
|
133
|
+
// Show queue
|
|
134
|
+
inflight: 0,
|
|
135
|
+
pending: [],
|
|
136
|
+
pendingSet: new Set(),
|
|
137
|
+
|
|
138
|
+
// Scheduler
|
|
112
139
|
runQueued: false,
|
|
113
|
-
burstActive: false,
|
|
140
|
+
burstActive: false,
|
|
141
|
+
burstDeadline: 0,
|
|
142
|
+
burstCount: 0,
|
|
143
|
+
lastBurstTs: 0,
|
|
144
|
+
firstShown: false,
|
|
114
145
|
};
|
|
115
146
|
|
|
116
147
|
const isBlocked = () => now() < state.blockedUntil;
|
|
@@ -124,10 +155,15 @@
|
|
|
124
155
|
|
|
125
156
|
async function fetchConfig() {
|
|
126
157
|
if (state.cfg) return state.cfg;
|
|
158
|
+
// Prefer inline config injected by server (zero latency)
|
|
127
159
|
try {
|
|
128
160
|
const inline = window.__nbbEzoicCfg;
|
|
129
|
-
if (inline && typeof inline === 'object') {
|
|
161
|
+
if (inline && typeof inline === 'object') {
|
|
162
|
+
state.cfg = inline;
|
|
163
|
+
return state.cfg;
|
|
164
|
+
}
|
|
130
165
|
} catch (_) {}
|
|
166
|
+
// Fallback to API
|
|
131
167
|
try {
|
|
132
168
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
133
169
|
if (r.ok) state.cfg = await r.json();
|
|
@@ -159,22 +195,27 @@
|
|
|
159
195
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
160
196
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
161
197
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
198
|
+
// DOM fallback
|
|
162
199
|
if (document.querySelector(SEL.category)) return 'categories';
|
|
163
200
|
if (document.querySelector(SEL.post)) return 'topic';
|
|
164
201
|
if (document.querySelector(SEL.topic)) return 'categoryTopics';
|
|
165
202
|
return 'other';
|
|
166
203
|
}
|
|
167
204
|
|
|
168
|
-
function getKind() {
|
|
205
|
+
function getKind() {
|
|
206
|
+
return state.kind || (state.kind = detectKind());
|
|
207
|
+
}
|
|
169
208
|
|
|
170
209
|
// ── DOM queries ────────────────────────────────────────────────────────────
|
|
171
210
|
|
|
172
211
|
function getPosts() {
|
|
173
|
-
const all = document.querySelectorAll(SEL.post)
|
|
212
|
+
const all = document.querySelectorAll(SEL.post);
|
|
213
|
+
const out = [];
|
|
174
214
|
for (let i = 0; i < all.length; i++) {
|
|
175
215
|
const el = all[i];
|
|
176
216
|
if (!el.isConnected) continue;
|
|
177
217
|
if (!el.querySelector('[component="post/content"]')) continue;
|
|
218
|
+
// Skip nested quotes / parent posts
|
|
178
219
|
const parent = el.parentElement?.closest(SEL.post);
|
|
179
220
|
if (parent && parent !== el) continue;
|
|
180
221
|
if (el.getAttribute('component') === 'post/parent') continue;
|
|
@@ -182,6 +223,7 @@
|
|
|
182
223
|
}
|
|
183
224
|
return out;
|
|
184
225
|
}
|
|
226
|
+
|
|
185
227
|
function getTopics() { return Array.from(document.querySelectorAll(SEL.topic)); }
|
|
186
228
|
function getCategories() { return Array.from(document.querySelectorAll(SEL.category)); }
|
|
187
229
|
|
|
@@ -189,10 +231,16 @@
|
|
|
189
231
|
|
|
190
232
|
function stableId(klass, el) {
|
|
191
233
|
const attr = KIND[klass]?.anchorAttr;
|
|
192
|
-
if (attr) {
|
|
234
|
+
if (attr) {
|
|
235
|
+
const v = el.getAttribute(attr);
|
|
236
|
+
if (v != null && v !== '') return v;
|
|
237
|
+
}
|
|
238
|
+
// Positional fallback
|
|
193
239
|
const children = el.parentElement?.children;
|
|
194
240
|
if (!children) return 'i0';
|
|
195
|
-
for (let i = 0; i < children.length; i++) {
|
|
241
|
+
for (let i = 0; i < children.length; i++) {
|
|
242
|
+
if (children[i] === el) return `i${i}`;
|
|
243
|
+
}
|
|
196
244
|
return 'i0';
|
|
197
245
|
}
|
|
198
246
|
|
|
@@ -204,30 +252,95 @@
|
|
|
204
252
|
}
|
|
205
253
|
|
|
206
254
|
function getWrapSet(klass) {
|
|
207
|
-
let
|
|
208
|
-
if (!
|
|
209
|
-
return
|
|
255
|
+
let set = state.wrapsByClass.get(klass);
|
|
256
|
+
if (!set) { set = new Set(); state.wrapsByClass.set(klass, set); }
|
|
257
|
+
return set;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Garbage collection (NodeBB post virtualization safe) ─────────────────
|
|
261
|
+
//
|
|
262
|
+
// NodeBB can remove large portions of the DOM during infinite scrolling
|
|
263
|
+
// (especially posts in topics). If a wrap is removed outside of our own
|
|
264
|
+
// dropWrap(), we must free its placeholder id; otherwise the pool appears
|
|
265
|
+
// exhausted and ads won't re-insert when the user scrolls back up.
|
|
266
|
+
|
|
267
|
+
function gcDisconnectedWraps() {
|
|
268
|
+
// 1) Clean wrapByKey
|
|
269
|
+
for (const [key, w] of Array.from(state.wrapByKey.entries())) {
|
|
270
|
+
if (!w?.isConnected) state.wrapByKey.delete(key);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 2) Clean wrapsByClass sets and free ids
|
|
274
|
+
for (const [klass, set] of Array.from(state.wrapsByClass.entries())) {
|
|
275
|
+
for (const w of Array.from(set)) {
|
|
276
|
+
if (w?.isConnected) continue;
|
|
277
|
+
set.delete(w);
|
|
278
|
+
try {
|
|
279
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
280
|
+
if (Number.isFinite(id)) {
|
|
281
|
+
state.mountedIds.delete(id);
|
|
282
|
+
state.phState.delete(id);
|
|
283
|
+
state.lastShow.delete(id);
|
|
284
|
+
}
|
|
285
|
+
} catch (_) {}
|
|
286
|
+
}
|
|
287
|
+
if (!set.size) state.wrapsByClass.delete(klass);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 3) Authoritative rebuild of mountedIds from the live DOM
|
|
291
|
+
try {
|
|
292
|
+
const live = new Set();
|
|
293
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
294
|
+
const id = parseInt(w.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
295
|
+
if (id > 0) live.add(id);
|
|
296
|
+
}
|
|
297
|
+
// Drop any ids that are no longer live
|
|
298
|
+
for (const id of Array.from(state.mountedIds)) {
|
|
299
|
+
if (!live.has(id)) {
|
|
300
|
+
state.mountedIds.delete(id);
|
|
301
|
+
state.phState.delete(id);
|
|
302
|
+
state.lastShow.delete(id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (_) {}
|
|
210
306
|
}
|
|
211
307
|
|
|
212
|
-
// ── Wrap lifecycle
|
|
308
|
+
// ── Wrap lifecycle detection ───────────────────────────────────────────────
|
|
213
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Check if a wrap element still has its corresponding anchor in the DOM.
|
|
312
|
+
* Uses O(1) registry lookup first, then sibling scan, then global querySelector.
|
|
313
|
+
*/
|
|
214
314
|
function wrapIsLive(wrap) {
|
|
215
315
|
if (!wrap?.classList?.contains(WRAP_CLASS)) return false;
|
|
216
316
|
const key = wrap.getAttribute(ATTR.ANCHOR);
|
|
217
317
|
if (!key) return false;
|
|
318
|
+
|
|
319
|
+
// Fast path: registry match
|
|
218
320
|
if (state.wrapByKey.get(key) === wrap) return wrap.isConnected;
|
|
321
|
+
|
|
322
|
+
// Parse key
|
|
219
323
|
const colonIdx = key.indexOf(':');
|
|
220
|
-
const klass
|
|
324
|
+
const klass = key.slice(0, colonIdx);
|
|
325
|
+
const anchorId = key.slice(colonIdx + 1);
|
|
221
326
|
const cfg = KIND[klass];
|
|
222
327
|
if (!cfg) return false;
|
|
328
|
+
|
|
329
|
+
// Sibling scan (cheap for adjacent anchors)
|
|
223
330
|
const parent = wrap.parentElement;
|
|
224
331
|
if (parent) {
|
|
225
332
|
const sel = `${cfg.baseTag || ''}[${cfg.anchorAttr}="${anchorId}"]`;
|
|
226
333
|
for (const sib of parent.children) {
|
|
227
|
-
if (sib !== wrap) {
|
|
334
|
+
if (sib !== wrap) {
|
|
335
|
+
try { if (sib.matches(sel)) return sib.isConnected; } catch (_) {}
|
|
336
|
+
}
|
|
228
337
|
}
|
|
229
338
|
}
|
|
230
|
-
|
|
339
|
+
|
|
340
|
+
// Global fallback (expensive, rare)
|
|
341
|
+
try {
|
|
342
|
+
return document.querySelector(`${cfg.sel}[${cfg.anchorAttr}="${anchorId}"]`)?.isConnected === true;
|
|
343
|
+
} catch (_) { return false; }
|
|
231
344
|
}
|
|
232
345
|
|
|
233
346
|
function adjacentWrap(el) {
|
|
@@ -242,18 +355,21 @@
|
|
|
242
355
|
if (!ph || !isFilled(ph)) return false;
|
|
243
356
|
wrap.classList.remove('is-empty');
|
|
244
357
|
const id = parseInt(wrap.getAttribute(ATTR.WRAPID) || '0', 10);
|
|
245
|
-
if (id > 0) state.phState.set(id, '
|
|
358
|
+
if (id > 0) state.phState.set(id, 'shown');
|
|
246
359
|
return true;
|
|
247
360
|
}
|
|
248
361
|
|
|
249
362
|
function scheduleUncollapseChecks(wrap) {
|
|
250
363
|
if (!wrap) return;
|
|
251
|
-
|
|
252
|
-
|
|
364
|
+
const delays = [500, 1500, 3000, 7000, 15000];
|
|
365
|
+
for (const ms of delays) {
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
try { clearEmptyIfFilled(wrap); } catch (_) {}
|
|
368
|
+
}, ms);
|
|
253
369
|
}
|
|
254
370
|
}
|
|
255
371
|
|
|
256
|
-
// ── Pool
|
|
372
|
+
// ── Pool management ────────────────────────────────────────────────────────
|
|
257
373
|
|
|
258
374
|
function pickId(poolKey) {
|
|
259
375
|
const pool = state.pools[poolKey];
|
|
@@ -267,153 +383,12 @@
|
|
|
267
383
|
return null;
|
|
268
384
|
}
|
|
269
385
|
|
|
270
|
-
// ──
|
|
271
|
-
//
|
|
272
|
-
// Correct Ezoic infinite scroll flow:
|
|
273
|
-
// First batch: ez.cmd.push(() => { ez.define(...ids); ez.enable(); })
|
|
274
|
-
// Next batches: ez.cmd.push(() => { ez.define(...ids); ez.displayMore(...ids); })
|
|
275
|
-
// Recycle: ez.cmd.push(() => { ez.destroyPlaceholders(id); })
|
|
276
|
-
// → wait → new DOM → ez.cmd.push(() => { ez.define(id); ez.displayMore(id); })
|
|
277
|
-
//
|
|
278
|
-
// We batch define+displayMore calls using ezBatch to avoid calling the
|
|
279
|
-
// Ezoic API on every single placeholder insertion.
|
|
280
|
-
|
|
281
|
-
function ezCmd(fn) {
|
|
282
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
283
|
-
const ez = window.ezstandalone;
|
|
284
|
-
if (Array.isArray(ez.cmd)) {
|
|
285
|
-
ez.cmd.push(fn);
|
|
286
|
-
} else {
|
|
287
|
-
try { fn(); } catch (_) {}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Queue a placeholder ID for the next batched define+displayMore call.
|
|
293
|
-
*/
|
|
294
|
-
function ezEnqueue(id) {
|
|
295
|
-
if (isBlocked()) return;
|
|
296
|
-
state.ezBatch.add(id);
|
|
297
|
-
if (!state.ezFlushTimer) {
|
|
298
|
-
state.ezFlushTimer = setTimeout(ezFlush, TIMING.BATCH_FLUSH_MS);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Flush: define new IDs with Ezoic, then call displayMore.
|
|
304
|
-
*
|
|
305
|
-
* IMPORTANT: We NEVER call ez.enable() — Ezoic's sa.min.js calls it
|
|
306
|
-
* automatically on page load. Calling it again causes
|
|
307
|
-
* "Enable should only ever be called once" error.
|
|
308
|
-
*
|
|
309
|
-
* We also check ez.getSelectedPlaceholders() or equivalent to skip
|
|
310
|
-
* IDs that Ezoic already knows about (defined during initial enable).
|
|
311
|
-
*/
|
|
312
|
-
function ezFlush() {
|
|
313
|
-
state.ezFlushTimer = null;
|
|
314
|
-
if (isBlocked() || !state.ezBatch.size) return;
|
|
315
|
-
|
|
316
|
-
// Filter to only valid, connected placeholders
|
|
317
|
-
const ids = [];
|
|
318
|
-
for (const id of state.ezBatch) {
|
|
319
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
320
|
-
if (!ph?.isConnected) { state.phState.delete(id); continue; }
|
|
321
|
-
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'displayed'); continue; }
|
|
322
|
-
ids.push(id);
|
|
323
|
-
}
|
|
324
|
-
state.ezBatch.clear();
|
|
325
|
-
if (!ids.length) return;
|
|
326
|
-
|
|
327
|
-
ezCmd(() => {
|
|
328
|
-
const ez = window.ezstandalone;
|
|
329
|
-
if (!ez) return;
|
|
330
|
-
|
|
331
|
-
// Check which IDs Ezoic already knows about to avoid "already defined"
|
|
332
|
-
const alreadyDefined = new Set();
|
|
333
|
-
try {
|
|
334
|
-
// ez.allPlaceholders is an array of IDs Ezoic has already seen
|
|
335
|
-
if (Array.isArray(ez.allPlaceholders)) {
|
|
336
|
-
for (const p of ez.allPlaceholders) alreadyDefined.add(Number(p));
|
|
337
|
-
}
|
|
338
|
-
} catch (_) {}
|
|
339
|
-
|
|
340
|
-
const toDefine = ids.filter(id => !alreadyDefined.has(id));
|
|
341
|
-
// All IDs get displayMore, but only new ones get define
|
|
342
|
-
const toDisplay = ids;
|
|
343
|
-
|
|
344
|
-
// define() only for truly new placeholders
|
|
345
|
-
if (toDefine.length) {
|
|
346
|
-
try {
|
|
347
|
-
if (typeof ez.define === 'function') {
|
|
348
|
-
ez.define(...toDefine);
|
|
349
|
-
}
|
|
350
|
-
} catch (_) {}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
for (const id of ids) state.phState.set(id, 'defined');
|
|
354
|
-
|
|
355
|
-
// displayMore() for all IDs — this is the infinite scroll API
|
|
356
|
-
if (toDisplay.length) {
|
|
357
|
-
try {
|
|
358
|
-
if (typeof ez.displayMore === 'function') {
|
|
359
|
-
ez.displayMore(...toDisplay);
|
|
360
|
-
}
|
|
361
|
-
} catch (_) {}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Mark as displayed and schedule fill checks
|
|
365
|
-
for (const id of ids) {
|
|
366
|
-
state.phState.set(id, 'displayed');
|
|
367
|
-
state.lastShow.set(id, now());
|
|
368
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
369
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
370
|
-
if (wrap) {
|
|
371
|
-
wrap.setAttribute(ATTR.SHOWN, String(now()));
|
|
372
|
-
scheduleUncollapseChecks(wrap);
|
|
373
|
-
}
|
|
374
|
-
scheduleEmptyCheck(id);
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
}
|
|
386
|
+
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
378
387
|
|
|
379
388
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
389
|
+
* When pool is exhausted, recycle a wrap far above the viewport.
|
|
390
|
+
* Sequence: destroy → delay → re-observe → enqueueShow
|
|
382
391
|
*/
|
|
383
|
-
function ezDestroy(id) {
|
|
384
|
-
return new Promise(resolve => {
|
|
385
|
-
ezCmd(() => {
|
|
386
|
-
state.phState.set(id, 'destroyed');
|
|
387
|
-
const ez = window.ezstandalone;
|
|
388
|
-
try {
|
|
389
|
-
if (typeof ez?.destroyPlaceholders === 'function') {
|
|
390
|
-
ez.destroyPlaceholders(id);
|
|
391
|
-
}
|
|
392
|
-
} catch (_) {}
|
|
393
|
-
setTimeout(resolve, TIMING.RECYCLE_DESTROY_MS);
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function scheduleEmptyCheck(id) {
|
|
399
|
-
const showTs = now();
|
|
400
|
-
setTimeout(() => {
|
|
401
|
-
try {
|
|
402
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
403
|
-
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
404
|
-
if (!wrap || !ph?.isConnected) return;
|
|
405
|
-
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
406
|
-
if (clearEmptyIfFilled(wrap)) return;
|
|
407
|
-
wrap.classList.add('is-empty');
|
|
408
|
-
} catch (_) {}
|
|
409
|
-
}, TIMING.EMPTY_CHECK_MS);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ── Recycling ──────────────────────────────────────────────────────────────
|
|
413
|
-
//
|
|
414
|
-
// When pool is exhausted, find the farthest wrap above viewport,
|
|
415
|
-
// destroy its Ezoic placeholder, recreate the DOM, then define+displayMore.
|
|
416
|
-
|
|
417
392
|
function recycleWrap(klass, targetEl, newKey) {
|
|
418
393
|
const ez = window.ezstandalone;
|
|
419
394
|
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
@@ -421,8 +396,7 @@
|
|
|
421
396
|
typeof ez?.displayMore !== 'function') return null;
|
|
422
397
|
|
|
423
398
|
const vh = window.innerHeight || 800;
|
|
424
|
-
const threshold = -
|
|
425
|
-
const t = now();
|
|
399
|
+
const threshold = -vh;
|
|
426
400
|
let bestEmpty = null, bestEmptyY = Infinity;
|
|
427
401
|
let bestFull = null, bestFullY = Infinity;
|
|
428
402
|
|
|
@@ -431,12 +405,6 @@
|
|
|
431
405
|
|
|
432
406
|
for (const wrap of wraps) {
|
|
433
407
|
try {
|
|
434
|
-
const created = parseInt(wrap.getAttribute(ATTR.CREATED) || '0', 10);
|
|
435
|
-
if (t - created < RECYCLE_MIN_AGE_MS) continue;
|
|
436
|
-
const wid = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
437
|
-
const st = state.phState.get(wid);
|
|
438
|
-
// Don't recycle placeholders that are still being processed
|
|
439
|
-
if (st === 'new' || st === 'defined') continue;
|
|
440
408
|
const bottom = wrap.getBoundingClientRect().bottom;
|
|
441
409
|
if (bottom > threshold) continue;
|
|
442
410
|
if (!isFilled(wrap)) {
|
|
@@ -455,46 +423,48 @@
|
|
|
455
423
|
|
|
456
424
|
const oldKey = best.getAttribute(ATTR.ANCHOR);
|
|
457
425
|
|
|
458
|
-
// Unobserve before moving
|
|
426
|
+
// Unobserve before moving to prevent stale showAds
|
|
459
427
|
try {
|
|
460
428
|
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
461
429
|
if (ph) state.io?.unobserve(ph);
|
|
462
430
|
} catch (_) {}
|
|
463
431
|
|
|
464
|
-
//
|
|
465
|
-
|
|
432
|
+
// Move the wrap to new position
|
|
433
|
+
mutate(() => {
|
|
434
|
+
best.setAttribute(ATTR.ANCHOR, newKey);
|
|
435
|
+
best.setAttribute(ATTR.CREATED, String(now()));
|
|
436
|
+
best.setAttribute(ATTR.SHOWN, '0');
|
|
437
|
+
best.classList.remove('is-empty');
|
|
438
|
+
best.replaceChildren();
|
|
439
|
+
|
|
440
|
+
const fresh = document.createElement('div');
|
|
441
|
+
fresh.id = `${PH_PREFIX}${id}`;
|
|
442
|
+
fresh.setAttribute('data-ezoic-id', String(id));
|
|
443
|
+
fresh.style.minHeight = '1px';
|
|
444
|
+
best.appendChild(fresh);
|
|
445
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
446
|
+
});
|
|
466
447
|
|
|
467
|
-
|
|
468
|
-
|
|
448
|
+
// Update registry
|
|
449
|
+
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
450
|
+
state.wrapByKey.set(newKey, best);
|
|
469
451
|
|
|
452
|
+
// Ezoic recycle sequence
|
|
453
|
+
const doDestroy = () => {
|
|
454
|
+
state.phState.set(id, 'destroyed');
|
|
455
|
+
try { ez.destroyPlaceholders(id); } catch (_) {
|
|
456
|
+
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
457
|
+
}
|
|
470
458
|
setTimeout(() => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
best.classList.remove('is-empty');
|
|
477
|
-
best.replaceChildren();
|
|
478
|
-
|
|
479
|
-
const fresh = document.createElement('div');
|
|
480
|
-
fresh.id = `${PH_PREFIX}${id}`;
|
|
481
|
-
fresh.setAttribute('data-ezoic-id', String(id));
|
|
482
|
-
fresh.style.minHeight = '1px';
|
|
483
|
-
best.appendChild(fresh);
|
|
484
|
-
targetEl.insertAdjacentElement('afterend', best);
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
if (oldKey && state.wrapByKey.get(oldKey) === best) state.wrapByKey.delete(oldKey);
|
|
488
|
-
state.wrapByKey.set(newKey, best);
|
|
459
|
+
try { observePlaceholder(id); } catch (_) {}
|
|
460
|
+
state.phState.set(id, 'new');
|
|
461
|
+
try { enqueueShow(id); } catch (_) {}
|
|
462
|
+
}, TIMING.RECYCLE_DELAY_MS);
|
|
463
|
+
};
|
|
489
464
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
observePlaceholder(id);
|
|
494
|
-
ezEnqueue(id);
|
|
495
|
-
}, TIMING.RECYCLE_DEFINE_MS);
|
|
496
|
-
}, TIMING.RECYCLE_DESTROY_MS);
|
|
497
|
-
});
|
|
465
|
+
try {
|
|
466
|
+
(typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy();
|
|
467
|
+
} catch (_) {}
|
|
498
468
|
|
|
499
469
|
return { id, wrap: best };
|
|
500
470
|
}
|
|
@@ -509,6 +479,7 @@
|
|
|
509
479
|
w.setAttribute(ATTR.CREATED, String(now()));
|
|
510
480
|
w.setAttribute(ATTR.SHOWN, '0');
|
|
511
481
|
w.style.cssText = 'width:100%;display:block';
|
|
482
|
+
|
|
512
483
|
const ph = document.createElement('div');
|
|
513
484
|
ph.id = `${PH_PREFIX}${id}`;
|
|
514
485
|
ph.setAttribute('data-ezoic-id', String(id));
|
|
@@ -521,8 +492,10 @@
|
|
|
521
492
|
if (!el?.insertAdjacentElement) return null;
|
|
522
493
|
if (findWrap(key)) return null;
|
|
523
494
|
if (state.mountedIds.has(id)) return null;
|
|
495
|
+
// Ensure no duplicate DOM element with same placeholder ID
|
|
524
496
|
const existing = document.getElementById(`${PH_PREFIX}${id}`);
|
|
525
497
|
if (existing?.isConnected) return null;
|
|
498
|
+
|
|
526
499
|
const w = makeWrap(id, klass, key);
|
|
527
500
|
mutate(() => el.insertAdjacentElement('afterend', w));
|
|
528
501
|
state.mountedIds.add(id);
|
|
@@ -536,13 +509,21 @@
|
|
|
536
509
|
try {
|
|
537
510
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
538
511
|
if (ph instanceof Element) state.io?.unobserve(ph);
|
|
512
|
+
|
|
539
513
|
const id = parseInt(w.getAttribute(ATTR.WRAPID), 10);
|
|
540
|
-
if (Number.isFinite(id)) {
|
|
514
|
+
if (Number.isFinite(id)) {
|
|
515
|
+
state.mountedIds.delete(id);
|
|
516
|
+
state.phState.delete(id);
|
|
517
|
+
}
|
|
518
|
+
|
|
541
519
|
const key = w.getAttribute(ATTR.ANCHOR);
|
|
542
520
|
if (key && state.wrapByKey.get(key) === w) state.wrapByKey.delete(key);
|
|
521
|
+
|
|
522
|
+
// Find the kind class to unregister
|
|
543
523
|
for (const cls of w.classList) {
|
|
544
524
|
if (cls !== WRAP_CLASS && cls.startsWith('ezoic-ad-')) {
|
|
545
|
-
state.wrapsByClass.get(cls)?.delete(w);
|
|
525
|
+
state.wrapsByClass.get(cls)?.delete(w);
|
|
526
|
+
break;
|
|
546
527
|
}
|
|
547
528
|
}
|
|
548
529
|
w.remove();
|
|
@@ -550,24 +531,33 @@
|
|
|
550
531
|
}
|
|
551
532
|
|
|
552
533
|
// ── Prune (category topic lists only) ──────────────────────────────────────
|
|
534
|
+
//
|
|
535
|
+
// Safe for category topic lists: NodeBB does NOT virtualize topics in categories.
|
|
536
|
+
// NOT safe for posts: NodeBB virtualizes posts off-viewport.
|
|
553
537
|
|
|
554
538
|
function pruneOrphansBetween() {
|
|
555
539
|
const klass = 'ezoic-ad-between';
|
|
556
540
|
const cfg = KIND[klass];
|
|
557
541
|
const wraps = state.wrapsByClass.get(klass);
|
|
558
542
|
if (!wraps?.size) return;
|
|
543
|
+
|
|
544
|
+
// Build set of live anchor IDs
|
|
559
545
|
const liveAnchors = new Set();
|
|
560
546
|
for (const el of document.querySelectorAll(`${cfg.baseTag}[${cfg.anchorAttr}]`)) {
|
|
561
547
|
const v = el.getAttribute(cfg.anchorAttr);
|
|
562
548
|
if (v) liveAnchors.add(v);
|
|
563
549
|
}
|
|
550
|
+
|
|
564
551
|
const t = now();
|
|
565
552
|
for (const w of wraps) {
|
|
566
553
|
const created = parseInt(w.getAttribute(ATTR.CREATED) || '0', 10);
|
|
567
554
|
if (t - created < TIMING.MIN_PRUNE_AGE_MS) continue;
|
|
555
|
+
|
|
568
556
|
const key = w.getAttribute(ATTR.ANCHOR) ?? '';
|
|
569
557
|
const sid = key.slice(klass.length + 1);
|
|
570
|
-
if (!sid || !liveAnchors.has(sid))
|
|
558
|
+
if (!sid || !liveAnchors.has(sid)) {
|
|
559
|
+
mutate(() => dropWrap(w));
|
|
560
|
+
}
|
|
571
561
|
}
|
|
572
562
|
}
|
|
573
563
|
|
|
@@ -579,6 +569,7 @@
|
|
|
579
569
|
const v = el.getAttribute(attr);
|
|
580
570
|
if (v != null && v !== '' && !isNaN(v)) return parseInt(v, 10);
|
|
581
571
|
}
|
|
572
|
+
// Positional fallback
|
|
582
573
|
const fullSel = KIND[klass]?.sel ?? '';
|
|
583
574
|
let i = 0;
|
|
584
575
|
for (const s of el.parentElement?.children ?? []) {
|
|
@@ -595,9 +586,11 @@
|
|
|
595
586
|
for (const el of items) {
|
|
596
587
|
if (inserted >= LIMITS.MAX_INSERTS_RUN) break;
|
|
597
588
|
if (!el?.isConnected) continue;
|
|
589
|
+
|
|
598
590
|
const ord = ordinal(klass, el);
|
|
599
591
|
if (!(showFirst && ord === 0) && (ord + 1) % interval !== 0) continue;
|
|
600
592
|
if (adjacentWrap(el)) continue;
|
|
593
|
+
|
|
601
594
|
const key = anchorKey(klass, el);
|
|
602
595
|
if (findWrap(key)) continue;
|
|
603
596
|
|
|
@@ -606,13 +599,12 @@
|
|
|
606
599
|
const w = insertAfter(el, id, klass, key);
|
|
607
600
|
if (w) {
|
|
608
601
|
observePlaceholder(id);
|
|
609
|
-
|
|
610
|
-
ezEnqueue(id);
|
|
602
|
+
if (!state.firstShown) { state.firstShown = true; enqueueShow(id); }
|
|
611
603
|
inserted++;
|
|
612
604
|
}
|
|
613
605
|
} else {
|
|
614
606
|
const recycled = recycleWrap(klass, el, key);
|
|
615
|
-
if (!recycled) break;
|
|
607
|
+
if (!recycled) break; // Pool truly exhausted
|
|
616
608
|
inserted++;
|
|
617
609
|
}
|
|
618
610
|
}
|
|
@@ -620,12 +612,6 @@
|
|
|
620
612
|
}
|
|
621
613
|
|
|
622
614
|
// ── IntersectionObserver ───────────────────────────────────────────────────
|
|
623
|
-
//
|
|
624
|
-
// The IO is used to eagerly observe placeholders so that when they enter
|
|
625
|
-
// the viewport margin, we can queue them for Ezoic. However, the actual
|
|
626
|
-
// Ezoic API calls (define/displayMore) happen in the batched flush.
|
|
627
|
-
// The IO callback is mainly useful for re-triggering after NodeBB
|
|
628
|
-
// virtualisation re-inserts posts.
|
|
629
615
|
|
|
630
616
|
function getIO() {
|
|
631
617
|
if (state.io) return state.io;
|
|
@@ -633,19 +619,9 @@
|
|
|
633
619
|
state.io = new IntersectionObserver(entries => {
|
|
634
620
|
for (const entry of entries) {
|
|
635
621
|
if (!entry.isIntersecting) continue;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (!id || id <= 0) continue;
|
|
640
|
-
const st = state.phState.get(id);
|
|
641
|
-
// Only enqueue if not yet processed by Ezoic
|
|
642
|
-
if (st === 'new') {
|
|
643
|
-
ezEnqueue(id);
|
|
644
|
-
} else if (st === 'displayed') {
|
|
645
|
-
// Already shown — check if the placeholder is actually filled.
|
|
646
|
-
// If not (Ezoic had no ad), don't re-trigger — it won't help.
|
|
647
|
-
// If yes, nothing to do.
|
|
648
|
-
}
|
|
622
|
+
if (entry.target instanceof Element) state.io?.unobserve(entry.target);
|
|
623
|
+
const id = parseInt(entry.target.getAttribute('data-ezoic-id'), 10);
|
|
624
|
+
if (id > 0) enqueueShow(id);
|
|
649
625
|
}
|
|
650
626
|
}, {
|
|
651
627
|
root: null,
|
|
@@ -658,16 +634,193 @@
|
|
|
658
634
|
|
|
659
635
|
function observePlaceholder(id) {
|
|
660
636
|
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
661
|
-
if (ph?.isConnected) {
|
|
637
|
+
if (ph?.isConnected) {
|
|
638
|
+
try { getIO()?.observe(ph); } catch (_) {}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ── Show queue ─────────────────────────────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
function enqueueShow(id) {
|
|
645
|
+
if (!id || isBlocked()) return;
|
|
646
|
+
const st = state.phState.get(id);
|
|
647
|
+
if (st === 'show-queued' || st === 'shown') return;
|
|
648
|
+
if (now() - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) return;
|
|
649
|
+
|
|
650
|
+
if (state.inflight >= LIMITS.MAX_INFLIGHT) {
|
|
651
|
+
if (!state.pendingSet.has(id)) {
|
|
652
|
+
state.pending.push(id);
|
|
653
|
+
state.pendingSet.add(id);
|
|
654
|
+
state.phState.set(id, 'show-queued');
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
state.phState.set(id, 'show-queued');
|
|
659
|
+
startShow(id);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function drainQueue() {
|
|
663
|
+
if (isBlocked()) return;
|
|
664
|
+
while (state.inflight < LIMITS.MAX_INFLIGHT && state.pending.length) {
|
|
665
|
+
const id = state.pending.shift();
|
|
666
|
+
state.pendingSet.delete(id);
|
|
667
|
+
startShow(id);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function startShow(id) {
|
|
672
|
+
if (!id || isBlocked()) return;
|
|
673
|
+
state.inflight++;
|
|
674
|
+
|
|
675
|
+
let released = false;
|
|
676
|
+
const release = () => {
|
|
677
|
+
if (released) return;
|
|
678
|
+
released = true;
|
|
679
|
+
state.inflight = Math.max(0, state.inflight - 1);
|
|
680
|
+
drainQueue();
|
|
681
|
+
};
|
|
682
|
+
const timer = setTimeout(release, TIMING.SHOW_TIMEOUT_MS);
|
|
683
|
+
|
|
684
|
+
requestAnimationFrame(() => {
|
|
685
|
+
try {
|
|
686
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
687
|
+
|
|
688
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
689
|
+
if (!ph?.isConnected) {
|
|
690
|
+
state.phState.delete(id);
|
|
691
|
+
clearTimeout(timer);
|
|
692
|
+
return release();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (isFilled(ph) || isPlaceholderUsed(ph)) {
|
|
696
|
+
state.phState.set(id, 'shown');
|
|
697
|
+
clearTimeout(timer);
|
|
698
|
+
return release();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const t = now();
|
|
702
|
+
if (t - (state.lastShow.get(id) ?? 0) < TIMING.SHOW_THROTTLE_MS) {
|
|
703
|
+
clearTimeout(timer);
|
|
704
|
+
return release();
|
|
705
|
+
}
|
|
706
|
+
state.lastShow.set(id, t);
|
|
707
|
+
|
|
708
|
+
try { ph.closest(`.${WRAP_CLASS}`)?.setAttribute(ATTR.SHOWN, String(t)); } catch (_) {}
|
|
709
|
+
state.phState.set(id, 'shown');
|
|
710
|
+
|
|
711
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
712
|
+
const ez = window.ezstandalone;
|
|
713
|
+
|
|
714
|
+
const doShow = () => {
|
|
715
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
716
|
+
try { ez.showAds(id); } catch (_) {}
|
|
717
|
+
if (wrap) scheduleUncollapseChecks(wrap);
|
|
718
|
+
scheduleEmptyCheck(id, t);
|
|
719
|
+
setTimeout(() => { clearTimeout(timer); release(); }, TIMING.SHOW_RELEASE_MS);
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
723
|
+
} catch (_) {
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
release();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function scheduleEmptyCheck(id, showTs) {
|
|
731
|
+
setTimeout(() => {
|
|
732
|
+
try {
|
|
733
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
734
|
+
const wrap = ph?.closest(`.${WRAP_CLASS}`);
|
|
735
|
+
if (!wrap || !ph?.isConnected) return;
|
|
736
|
+
// Skip if a newer show happened since
|
|
737
|
+
if (parseInt(wrap.getAttribute(ATTR.SHOWN) || '0', 10) > showTs) return;
|
|
738
|
+
if (clearEmptyIfFilled(wrap)) return;
|
|
739
|
+
wrap.classList.add('is-empty');
|
|
740
|
+
} catch (_) {}
|
|
741
|
+
}, TIMING.EMPTY_CHECK_MS);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
745
|
+
//
|
|
746
|
+
// Intercepts ez.showAds() to:
|
|
747
|
+
// - block calls during navigation transitions
|
|
748
|
+
// - filter out disconnected placeholders
|
|
749
|
+
// - batch calls for efficiency
|
|
750
|
+
|
|
751
|
+
function patchShowAds() {
|
|
752
|
+
const apply = () => {
|
|
753
|
+
try {
|
|
754
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
755
|
+
const ez = window.ezstandalone;
|
|
756
|
+
if (window.__nbbEzPatched || typeof ez.showAds !== 'function') return;
|
|
757
|
+
window.__nbbEzPatched = true;
|
|
758
|
+
|
|
759
|
+
const orig = ez.showAds.bind(ez);
|
|
760
|
+
const queue = new Set();
|
|
761
|
+
let flushTimer = null;
|
|
762
|
+
|
|
763
|
+
const flush = () => {
|
|
764
|
+
flushTimer = null;
|
|
765
|
+
if (isBlocked() || !queue.size) return;
|
|
766
|
+
|
|
767
|
+
const ids = Array.from(queue).sort((a, b) => a - b);
|
|
768
|
+
queue.clear();
|
|
769
|
+
|
|
770
|
+
const valid = ids.filter(id => {
|
|
771
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
772
|
+
if (!ph?.isConnected) { state.phState.delete(id); return false; }
|
|
773
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); return false; }
|
|
774
|
+
return true;
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
for (let i = 0; i < valid.length; i += LIMITS.BATCH_SIZE) {
|
|
778
|
+
const chunk = valid.slice(i, i + LIMITS.BATCH_SIZE);
|
|
779
|
+
try { orig(...chunk); } catch (_) {
|
|
780
|
+
for (const cid of chunk) { try { orig(cid); } catch (_) {} }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
ez.showAds = function (...args) {
|
|
786
|
+
if (isBlocked()) return;
|
|
787
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
788
|
+
for (const v of ids) {
|
|
789
|
+
const id = parseInt(v, 10);
|
|
790
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
791
|
+
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
792
|
+
if (!ph?.isConnected) continue;
|
|
793
|
+
if (isPlaceholderUsed(ph)) { state.phState.set(id, 'shown'); continue; }
|
|
794
|
+
state.phState.set(id, 'show-queued');
|
|
795
|
+
queue.add(id);
|
|
796
|
+
}
|
|
797
|
+
if (queue.size && !flushTimer) {
|
|
798
|
+
flushTimer = setTimeout(flush, TIMING.BATCH_FLUSH_MS);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
} catch (_) {}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
apply();
|
|
805
|
+
if (!window.__nbbEzPatched) {
|
|
806
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
807
|
+
(window.ezstandalone.cmd = window.ezstandalone.cmd || []).push(apply);
|
|
808
|
+
}
|
|
662
809
|
}
|
|
663
810
|
|
|
664
811
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
665
812
|
|
|
666
813
|
async function runCore() {
|
|
667
814
|
if (isBlocked()) return 0;
|
|
815
|
+
|
|
816
|
+
// Keep internal pools in sync with NodeBB DOM virtualization/removals
|
|
817
|
+
// so ads can re-insert correctly when users scroll back up.
|
|
818
|
+
try { gcDisconnectedWraps(); } catch (_) {}
|
|
819
|
+
|
|
668
820
|
const cfg = await fetchConfig();
|
|
669
821
|
if (!cfg || cfg.excluded) return 0;
|
|
670
822
|
initPools(cfg);
|
|
823
|
+
|
|
671
824
|
const kind = getKind();
|
|
672
825
|
if (kind === 'other') return 0;
|
|
673
826
|
|
|
@@ -678,16 +831,24 @@
|
|
|
678
831
|
};
|
|
679
832
|
|
|
680
833
|
if (kind === 'topic') {
|
|
681
|
-
return exec(
|
|
682
|
-
|
|
834
|
+
return exec(
|
|
835
|
+
'ezoic-ad-message', getPosts,
|
|
836
|
+
cfg.enableMessageAds, cfg.messageIntervalPosts, cfg.showFirstMessageAd, 'posts'
|
|
837
|
+
);
|
|
683
838
|
}
|
|
839
|
+
|
|
684
840
|
if (kind === 'categoryTopics') {
|
|
685
841
|
pruneOrphansBetween();
|
|
686
|
-
return exec(
|
|
687
|
-
|
|
842
|
+
return exec(
|
|
843
|
+
'ezoic-ad-between', getTopics,
|
|
844
|
+
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
845
|
+
);
|
|
688
846
|
}
|
|
689
|
-
|
|
690
|
-
|
|
847
|
+
|
|
848
|
+
return exec(
|
|
849
|
+
'ezoic-ad-categories', getCategories,
|
|
850
|
+
cfg.enableCategoryAds, cfg.intervalCategories, cfg.showFirstCategoryAd, 'categories'
|
|
851
|
+
);
|
|
691
852
|
}
|
|
692
853
|
|
|
693
854
|
// ── Scheduler & burst ──────────────────────────────────────────────────────
|
|
@@ -711,68 +872,94 @@
|
|
|
711
872
|
state.lastBurstTs = t;
|
|
712
873
|
state.pageKey = pageKey();
|
|
713
874
|
state.burstDeadline = t + LIMITS.BURST_WINDOW_MS;
|
|
875
|
+
|
|
714
876
|
if (state.burstActive) return;
|
|
715
877
|
state.burstActive = true;
|
|
716
878
|
state.burstCount = 0;
|
|
879
|
+
|
|
717
880
|
const step = () => {
|
|
718
881
|
if (pageKey() !== state.pageKey || isBlocked() || now() > state.burstDeadline || state.burstCount >= LIMITS.MAX_BURST_STEPS) {
|
|
719
|
-
state.burstActive = false;
|
|
882
|
+
state.burstActive = false;
|
|
883
|
+
return;
|
|
720
884
|
}
|
|
721
885
|
state.burstCount++;
|
|
722
886
|
scheduleRun(n => {
|
|
723
|
-
if (!n && !state.
|
|
887
|
+
if (!n && !state.pending.length) { state.burstActive = false; return; }
|
|
724
888
|
setTimeout(step, n > 0 ? 150 : 300);
|
|
725
889
|
});
|
|
726
890
|
};
|
|
727
891
|
step();
|
|
728
892
|
}
|
|
729
893
|
|
|
730
|
-
// ── Cleanup
|
|
894
|
+
// ── Cleanup on navigation ──────────────────────────────────────────────────
|
|
731
895
|
|
|
732
896
|
function cleanup() {
|
|
733
897
|
state.blockedUntil = now() + TIMING.BLOCK_DURATION_MS;
|
|
734
|
-
|
|
735
|
-
// Cancel pending Ezoic batch
|
|
736
|
-
if (state.ezFlushTimer) { clearTimeout(state.ezFlushTimer); state.ezFlushTimer = null; }
|
|
737
|
-
state.ezBatch.clear();
|
|
738
|
-
|
|
739
898
|
mutate(() => {
|
|
740
|
-
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`))
|
|
899
|
+
for (const w of document.querySelectorAll(`.${WRAP_CLASS}`)) {
|
|
900
|
+
dropWrap(w);
|
|
901
|
+
}
|
|
741
902
|
});
|
|
742
|
-
state.cfg
|
|
743
|
-
state.poolsReady
|
|
744
|
-
state.pools
|
|
745
|
-
state.cursors
|
|
903
|
+
state.cfg = null;
|
|
904
|
+
state.poolsReady = false;
|
|
905
|
+
state.pools = { topics: [], posts: [], categories: [] };
|
|
906
|
+
state.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
746
907
|
state.mountedIds.clear();
|
|
747
908
|
state.lastShow.clear();
|
|
748
909
|
state.wrapByKey.clear();
|
|
749
910
|
state.wrapsByClass.clear();
|
|
750
|
-
state.kind
|
|
911
|
+
state.kind = null;
|
|
751
912
|
state.phState.clear();
|
|
913
|
+
state.inflight = 0;
|
|
914
|
+
state.pending = [];
|
|
915
|
+
state.pendingSet.clear();
|
|
752
916
|
state.burstActive = false;
|
|
753
917
|
state.runQueued = false;
|
|
918
|
+
state.firstShown = false;
|
|
754
919
|
}
|
|
755
920
|
|
|
756
921
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
922
|
+
//
|
|
923
|
+
// Scoped to detect: (1) ad fill events in wraps, (2) new content items
|
|
757
924
|
|
|
758
925
|
function ensureDomObserver() {
|
|
759
926
|
if (state.domObs) return;
|
|
927
|
+
|
|
760
928
|
state.domObs = new MutationObserver(muts => {
|
|
761
929
|
if (state.mutGuard > 0 || isBlocked()) return;
|
|
930
|
+
|
|
762
931
|
let needsBurst = false;
|
|
932
|
+
|
|
933
|
+
// Determine relevant selectors for current page kind
|
|
763
934
|
const kind = getKind();
|
|
764
935
|
const relevantSels =
|
|
765
|
-
kind === 'topic'
|
|
766
|
-
kind === 'categoryTopics'
|
|
767
|
-
kind === 'categories'
|
|
768
|
-
|
|
936
|
+
kind === 'topic' ? [SEL.post] :
|
|
937
|
+
kind === 'categoryTopics'? [SEL.topic] :
|
|
938
|
+
kind === 'categories' ? [SEL.category] :
|
|
939
|
+
[SEL.post, SEL.topic, SEL.category];
|
|
769
940
|
|
|
770
941
|
for (const m of muts) {
|
|
771
942
|
if (m.type !== 'childList') continue;
|
|
943
|
+
|
|
944
|
+
// If NodeBB removed wraps as part of virtualization, free ids immediately
|
|
945
|
+
for (const node of m.removedNodes) {
|
|
946
|
+
if (!(node instanceof Element)) continue;
|
|
947
|
+
try {
|
|
948
|
+
if (node.classList?.contains(WRAP_CLASS)) {
|
|
949
|
+
dropWrap(node);
|
|
950
|
+
} else {
|
|
951
|
+
const wraps = node.querySelectorAll?.(`.${WRAP_CLASS}`);
|
|
952
|
+
if (wraps?.length) {
|
|
953
|
+
for (const w of wraps) dropWrap(w);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch (_) {}
|
|
957
|
+
}
|
|
958
|
+
|
|
772
959
|
for (const node of m.addedNodes) {
|
|
773
960
|
if (!(node instanceof Element)) continue;
|
|
774
961
|
|
|
775
|
-
//
|
|
962
|
+
// Check for ad fill events in wraps
|
|
776
963
|
try {
|
|
777
964
|
if (node.matches?.(FILL_SEL) || node.querySelector?.(FILL_SEL)) {
|
|
778
965
|
const wrap = node.closest?.(`.${WRAP_CLASS}.is-empty`) ||
|
|
@@ -781,121 +968,178 @@
|
|
|
781
968
|
}
|
|
782
969
|
} catch (_) {}
|
|
783
970
|
|
|
784
|
-
//
|
|
785
|
-
try {
|
|
786
|
-
const wraps = node.classList?.contains(WRAP_CLASS)
|
|
787
|
-
? [node]
|
|
788
|
-
: Array.from(node.querySelectorAll?.(`.${WRAP_CLASS}`) || []);
|
|
789
|
-
for (const wrap of wraps) {
|
|
790
|
-
const id = parseInt(wrap.getAttribute(ATTR.WRAPID), 10);
|
|
791
|
-
if (!id || id <= 0) continue;
|
|
792
|
-
const ph = wrap.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
793
|
-
if (ph) { try { getIO()?.observe(ph); } catch (_) {} }
|
|
794
|
-
}
|
|
795
|
-
} catch (_) {}
|
|
796
|
-
|
|
797
|
-
// New content detection
|
|
971
|
+
// Check for new content items (posts, topics, categories)
|
|
798
972
|
if (!needsBurst) {
|
|
799
973
|
for (const sel of relevantSels) {
|
|
800
974
|
try {
|
|
801
|
-
if (node.matches(sel) || node.querySelector(sel)) {
|
|
975
|
+
if (node.matches(sel) || node.querySelector(sel)) {
|
|
976
|
+
needsBurst = true;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
802
979
|
} catch (_) {}
|
|
803
980
|
}
|
|
804
981
|
}
|
|
805
982
|
}
|
|
806
983
|
if (needsBurst) break;
|
|
807
984
|
}
|
|
985
|
+
|
|
808
986
|
if (needsBurst) requestBurst();
|
|
809
987
|
});
|
|
810
|
-
|
|
988
|
+
|
|
989
|
+
try {
|
|
990
|
+
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
991
|
+
} catch (_) {}
|
|
811
992
|
}
|
|
812
993
|
|
|
813
|
-
// ── TCF / CMP Protection
|
|
994
|
+
// ── TCF / CMP Protection ─────────────────────────────────────────────────
|
|
995
|
+
//
|
|
996
|
+
// Root cause of the CMP errors:
|
|
997
|
+
// "Cannot read properties of null (reading 'postMessage')"
|
|
998
|
+
// "Cannot set properties of null (setting 'addtlConsent')"
|
|
999
|
+
//
|
|
1000
|
+
// The CMP (Gatekeeper Consent) communicates via postMessage on the
|
|
1001
|
+
// __tcfapiLocator iframe's contentWindow. During NodeBB ajaxify navigation,
|
|
1002
|
+
// jQuery's html() or empty() on the content area can cascade and remove
|
|
1003
|
+
// iframes from <body>. The CMP then calls getTCData on a stale reference
|
|
1004
|
+
// where contentWindow is null.
|
|
1005
|
+
//
|
|
1006
|
+
// Strategy (3 layers):
|
|
1007
|
+
//
|
|
1008
|
+
// 1. PROTECT: Move the locator iframe into <head> where ajaxify never
|
|
1009
|
+
// touches it. The TCF spec only requires the iframe to exist in the
|
|
1010
|
+
// document with name="__tcfapiLocator" — it works from <head>.
|
|
1011
|
+
//
|
|
1012
|
+
// 2. GUARD: Wrap __tcfapi and __cmp with a safety layer that catches
|
|
1013
|
+
// errors in the CMP's internal getTCData, preventing the uncaught
|
|
1014
|
+
// TypeError from propagating.
|
|
1015
|
+
//
|
|
1016
|
+
// 3. RESTORE: MutationObserver on <body> childList (not subtree) to
|
|
1017
|
+
// immediately re-create the locator if something still removes it.
|
|
814
1018
|
|
|
815
1019
|
function ensureTcfLocator() {
|
|
816
1020
|
if (!window.__tcfapi && !window.__cmp) return;
|
|
1021
|
+
|
|
817
1022
|
const LOCATOR_ID = '__tcfapiLocator';
|
|
1023
|
+
|
|
1024
|
+
// Create or relocate the locator iframe into <head> for protection
|
|
818
1025
|
const ensureInHead = () => {
|
|
819
1026
|
let existing = document.getElementById(LOCATOR_ID);
|
|
820
1027
|
if (existing) {
|
|
1028
|
+
// If it's in <body>, move it to <head> where ajaxify can't reach it
|
|
821
1029
|
if (existing.parentElement !== document.head) {
|
|
822
1030
|
try { document.head.appendChild(existing); } catch (_) {}
|
|
823
1031
|
}
|
|
824
|
-
return;
|
|
1032
|
+
return existing;
|
|
825
1033
|
}
|
|
1034
|
+
// Create fresh
|
|
826
1035
|
const f = document.createElement('iframe');
|
|
827
|
-
f.style.display = 'none';
|
|
1036
|
+
f.style.display = 'none';
|
|
1037
|
+
f.id = f.name = LOCATOR_ID;
|
|
828
1038
|
try { document.head.appendChild(f); } catch (_) {
|
|
1039
|
+
// Fallback to body if head insertion fails
|
|
829
1040
|
(document.body || document.documentElement).appendChild(f);
|
|
830
1041
|
}
|
|
1042
|
+
return f;
|
|
831
1043
|
};
|
|
1044
|
+
|
|
832
1045
|
ensureInHead();
|
|
1046
|
+
|
|
1047
|
+
// Layer 2: Guard the CMP API calls against null contentWindow
|
|
833
1048
|
if (!window.__nbbCmpGuarded) {
|
|
834
1049
|
window.__nbbCmpGuarded = true;
|
|
1050
|
+
|
|
1051
|
+
// Wrap __tcfapi
|
|
835
1052
|
if (typeof window.__tcfapi === 'function') {
|
|
836
|
-
const
|
|
837
|
-
window.__tcfapi = function (cmd,
|
|
838
|
-
try {
|
|
839
|
-
|
|
1053
|
+
const origTcf = window.__tcfapi;
|
|
1054
|
+
window.__tcfapi = function (cmd, version, cb, param) {
|
|
1055
|
+
try {
|
|
1056
|
+
return origTcf.call(this, cmd, version, function (...args) {
|
|
1057
|
+
try { cb?.(...args); } catch (_) {}
|
|
1058
|
+
}, param);
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
// If the error is the null postMessage/addtlConsent, swallow it
|
|
1061
|
+
if (e?.message?.includes('null')) {
|
|
1062
|
+
// Re-ensure locator exists, then retry once
|
|
1063
|
+
ensureInHead();
|
|
1064
|
+
try { return origTcf.call(this, cmd, version, cb, param); } catch (_) {}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
840
1067
|
};
|
|
841
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
// Wrap __cmp (legacy CMP v1 API)
|
|
842
1071
|
if (typeof window.__cmp === 'function') {
|
|
843
|
-
const
|
|
844
|
-
window.__cmp = function (...
|
|
845
|
-
try {
|
|
846
|
-
|
|
1072
|
+
const origCmp = window.__cmp;
|
|
1073
|
+
window.__cmp = function (...args) {
|
|
1074
|
+
try {
|
|
1075
|
+
return origCmp.apply(this, args);
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
if (e?.message?.includes('null')) {
|
|
1078
|
+
ensureInHead();
|
|
1079
|
+
try { return origCmp.apply(this, args); } catch (_) {}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
847
1082
|
};
|
|
848
1083
|
}
|
|
849
1084
|
}
|
|
1085
|
+
|
|
1086
|
+
// Layer 3: MutationObserver to immediately restore if removed
|
|
850
1087
|
if (!window.__nbbTcfObs) {
|
|
851
|
-
window.__nbbTcfObs = new MutationObserver(
|
|
852
|
-
|
|
1088
|
+
window.__nbbTcfObs = new MutationObserver(muts => {
|
|
1089
|
+
// Fast check: still in document?
|
|
1090
|
+
if (document.getElementById(LOCATOR_ID)) return;
|
|
1091
|
+
// Something removed it — restore immediately (no debounce)
|
|
1092
|
+
ensureInHead();
|
|
853
1093
|
});
|
|
854
|
-
|
|
855
|
-
try {
|
|
1094
|
+
// Observe body direct children only (the most likely removal point)
|
|
1095
|
+
try {
|
|
1096
|
+
window.__nbbTcfObs.observe(document.body || document.documentElement, {
|
|
1097
|
+
childList: true,
|
|
1098
|
+
subtree: false,
|
|
1099
|
+
});
|
|
1100
|
+
} catch (_) {}
|
|
1101
|
+
// Also observe <head> in case something cleans it
|
|
1102
|
+
try {
|
|
1103
|
+
if (document.head) {
|
|
1104
|
+
window.__nbbTcfObs.observe(document.head, {
|
|
1105
|
+
childList: true,
|
|
1106
|
+
subtree: false,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
} catch (_) {}
|
|
856
1110
|
}
|
|
857
1111
|
}
|
|
858
1112
|
|
|
859
|
-
// ── aria-hidden protection ─────────────────────────────────────────────────
|
|
860
|
-
|
|
861
|
-
function protectAriaHidden() {
|
|
862
|
-
if (window.__nbbAriaObs) return;
|
|
863
|
-
const remove = () => {
|
|
864
|
-
try { if (document.body.getAttribute('aria-hidden') === 'true') document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
865
|
-
};
|
|
866
|
-
remove();
|
|
867
|
-
window.__nbbAriaObs = new MutationObserver(remove);
|
|
868
|
-
try { window.__nbbAriaObs.observe(document.body, { attributes: true, attributeFilter: ['aria-hidden'] }); } catch (_) {}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
1113
|
// ── Console muting ─────────────────────────────────────────────────────────
|
|
1114
|
+
//
|
|
1115
|
+
// Mute noisy Ezoic warnings that are expected in infinite scroll context.
|
|
1116
|
+
// Uses startsWith checks instead of includes for performance.
|
|
872
1117
|
|
|
873
1118
|
function muteConsole() {
|
|
874
1119
|
if (window.__nbbEzMuted) return;
|
|
875
1120
|
window.__nbbEzMuted = true;
|
|
1121
|
+
|
|
876
1122
|
const PREFIXES = [
|
|
877
1123
|
'[EzoicAds JS]: Placeholder Id',
|
|
1124
|
+
'No valid placeholders for loadMore',
|
|
878
1125
|
'cannot call refresh on the same page',
|
|
879
1126
|
'no placeholders are currently defined in Refresh',
|
|
880
1127
|
'Debugger iframe already exists',
|
|
881
1128
|
'[CMP] Error in custom getTCData',
|
|
882
1129
|
'vignette: no interstitial API',
|
|
883
1130
|
];
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
'adsbygoogle.push() error: All',
|
|
887
|
-
'has already been defined',
|
|
888
|
-
'No valid placeholders for loadMore',
|
|
889
|
-
'bad response. Status',
|
|
890
|
-
];
|
|
1131
|
+
const PH_PATTERN = `with id ${PH_PREFIX}`;
|
|
1132
|
+
|
|
891
1133
|
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
892
1134
|
const orig = console[method];
|
|
893
1135
|
if (typeof orig !== 'function') continue;
|
|
894
1136
|
console[method] = function (...args) {
|
|
895
1137
|
if (typeof args[0] === 'string') {
|
|
896
1138
|
const msg = args[0];
|
|
897
|
-
for (const
|
|
898
|
-
|
|
1139
|
+
for (const prefix of PREFIXES) {
|
|
1140
|
+
if (msg.startsWith(prefix)) return;
|
|
1141
|
+
}
|
|
1142
|
+
if (msg.includes(PH_PATTERN)) return;
|
|
899
1143
|
}
|
|
900
1144
|
return orig.apply(console, args);
|
|
901
1145
|
};
|
|
@@ -903,24 +1147,31 @@
|
|
|
903
1147
|
}
|
|
904
1148
|
|
|
905
1149
|
// ── Network warmup ─────────────────────────────────────────────────────────
|
|
1150
|
+
// Run once per session — preconnect hints are in <head> via server-side injection
|
|
906
1151
|
|
|
907
1152
|
let _networkWarmed = false;
|
|
1153
|
+
|
|
908
1154
|
function warmNetwork() {
|
|
909
1155
|
if (_networkWarmed) return;
|
|
910
1156
|
_networkWarmed = true;
|
|
1157
|
+
|
|
911
1158
|
const head = document.head;
|
|
912
1159
|
if (!head) return;
|
|
913
|
-
|
|
1160
|
+
|
|
1161
|
+
const hints = [
|
|
914
1162
|
['preconnect', 'https://g.ezoic.net', true ],
|
|
915
1163
|
['preconnect', 'https://go.ezoic.net', true ],
|
|
916
1164
|
['preconnect', 'https://securepubads.g.doubleclick.net', true ],
|
|
917
1165
|
['preconnect', 'https://pagead2.googlesyndication.com', true ],
|
|
918
1166
|
['dns-prefetch', 'https://g.ezoic.net', false],
|
|
919
1167
|
['dns-prefetch', 'https://securepubads.g.doubleclick.net', false],
|
|
920
|
-
]
|
|
1168
|
+
];
|
|
1169
|
+
|
|
1170
|
+
for (const [rel, href, cors] of hints) {
|
|
921
1171
|
if (head.querySelector(`link[rel="${rel}"][href="${href}"]`)) continue;
|
|
922
1172
|
const link = document.createElement('link');
|
|
923
|
-
link.rel = rel;
|
|
1173
|
+
link.rel = rel;
|
|
1174
|
+
link.href = href;
|
|
924
1175
|
if (cors) link.crossOrigin = 'anonymous';
|
|
925
1176
|
head.appendChild(link);
|
|
926
1177
|
}
|
|
@@ -931,25 +1182,51 @@
|
|
|
931
1182
|
function bindNodeBB() {
|
|
932
1183
|
const $ = window.jQuery;
|
|
933
1184
|
if (!$) return;
|
|
1185
|
+
|
|
934
1186
|
$(window).off('.nbbEzoic');
|
|
1187
|
+
|
|
935
1188
|
$(window).on('action:ajaxify.start.nbbEzoic', cleanup);
|
|
1189
|
+
|
|
936
1190
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
937
|
-
state.pageKey
|
|
938
|
-
state.kind
|
|
1191
|
+
state.pageKey = pageKey();
|
|
1192
|
+
state.kind = null;
|
|
939
1193
|
state.blockedUntil = 0;
|
|
940
|
-
|
|
941
|
-
|
|
1194
|
+
|
|
1195
|
+
// Fix: CMP modal can leave aria-hidden="true" on <body> after navigation
|
|
1196
|
+
try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
|
|
1197
|
+
|
|
1198
|
+
muteConsole();
|
|
1199
|
+
ensureTcfLocator();
|
|
1200
|
+
warmNetwork();
|
|
1201
|
+
patchShowAds();
|
|
1202
|
+
getIO();
|
|
1203
|
+
ensureDomObserver();
|
|
1204
|
+
requestBurst();
|
|
942
1205
|
});
|
|
1206
|
+
|
|
1207
|
+
// Content-loaded events trigger burst
|
|
943
1208
|
const burstEvents = [
|
|
944
|
-
'action:ajaxify.contentLoaded',
|
|
945
|
-
'action:
|
|
1209
|
+
'action:ajaxify.contentLoaded',
|
|
1210
|
+
'action:posts.loaded',
|
|
1211
|
+
'action:topics.loaded',
|
|
1212
|
+
'action:categories.loaded',
|
|
1213
|
+
'action:category.loaded',
|
|
1214
|
+
'action:topic.loaded',
|
|
946
1215
|
].map(e => `${e}.nbbEzoic`).join(' ');
|
|
1216
|
+
|
|
947
1217
|
$(window).on(burstEvents, () => { if (!isBlocked()) requestBurst(); });
|
|
1218
|
+
|
|
1219
|
+
// Also bind via NodeBB hooks module (for compatibility)
|
|
948
1220
|
try {
|
|
949
1221
|
require(['hooks'], hooks => {
|
|
950
1222
|
if (typeof hooks?.on !== 'function') return;
|
|
951
|
-
for (const ev of [
|
|
952
|
-
|
|
1223
|
+
for (const ev of [
|
|
1224
|
+
'action:ajaxify.end',
|
|
1225
|
+
'action:posts.loaded',
|
|
1226
|
+
'action:topics.loaded',
|
|
1227
|
+
'action:categories.loaded',
|
|
1228
|
+
'action:topic.loaded',
|
|
1229
|
+
]) {
|
|
953
1230
|
try { hooks.on(ev, () => { if (!isBlocked()) requestBurst(); }); } catch (_) {}
|
|
954
1231
|
}
|
|
955
1232
|
});
|
|
@@ -961,7 +1238,10 @@
|
|
|
961
1238
|
window.addEventListener('scroll', () => {
|
|
962
1239
|
if (ticking) return;
|
|
963
1240
|
ticking = true;
|
|
964
|
-
requestAnimationFrame(() => {
|
|
1241
|
+
requestAnimationFrame(() => {
|
|
1242
|
+
ticking = false;
|
|
1243
|
+
requestBurst();
|
|
1244
|
+
});
|
|
965
1245
|
}, { passive: true });
|
|
966
1246
|
}
|
|
967
1247
|
|
|
@@ -970,8 +1250,8 @@
|
|
|
970
1250
|
state.pageKey = pageKey();
|
|
971
1251
|
muteConsole();
|
|
972
1252
|
ensureTcfLocator();
|
|
973
|
-
protectAriaHidden();
|
|
974
1253
|
warmNetwork();
|
|
1254
|
+
patchShowAds();
|
|
975
1255
|
getIO();
|
|
976
1256
|
ensureDomObserver();
|
|
977
1257
|
bindNodeBB();
|