nodebb-plugin-ezoic-infinite 1.4.23 → 1.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +607 -725
package/public/client.js CHANGED
@@ -4,851 +4,733 @@
4
4
  const $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
5
5
 
6
6
  const SELECTORS = {
7
- topicItem: 'li[component="category/topic"]',
8
- postItem: '[component="post"][data-pid]',
9
- categoryItem: 'li[component="categories/category"]',
10
- };
11
-
12
- const WRAP_CLASS = 'ezoic-ad';
13
- const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
14
-
15
- const MAX_INSERTS_PER_RUN = 3;
7
+ topicItem: 'li[component="category/topic"]',
8
+ postItem: '[component="post"][data-pid]',
9
+ categoryItem: 'li[component="categories/category"]',
10
+ }, WRAP_CLASS = 'ezoic-ad';
11
+ const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-', MAX_INSERTS_PER_RUN = 3;
16
12
 
17
13
  // IDs ayant été définis par Ezoic au moins une fois dans cette session navigateur.
18
14
  // Survit aux navigations ajaxify (contrairement à state.definedIds remis à zéro dans cleanup).
19
15
  // Nécessaire pour savoir si on doit appeler destroyPlaceholders avant recyclage.
20
16
  const sessionDefinedIds = new Set();
21
-
17
+
22
18
  // IDs actuellement en cours d'insertion (lock pour éviter les doublons de race condition)
23
- const insertingIds = new Set();
24
-
25
- const state = {
26
- pageKey: null,
27
- cfg: null,
28
- cfgPromise: null,
29
-
30
- poolTopics: [],
31
- poolPosts: [],
32
- poolCategories: [],
33
-
34
- usedTopics: new Set(),
35
- usedPosts: new Set(),
36
- usedCategories: new Set(),
37
-
38
- // wrappers currently on page (FIFO for recycling)
39
- liveBetween: [],
40
- liveMessage: [],
41
- liveCategory: [],
42
-
43
- lastShowById: new Map(),
44
- pendingById: new Set(),
45
- retryById: new Map(),
46
- retryTimer: null,
47
- retryQueue: [],
48
- retryQueueSet: new Set(),
49
- retryQueueRunning: false,
50
- badIds: new Set(),
51
- definedIds: new Set(),
52
-
53
- scheduled: false,
54
- timer: null,
55
-
56
- obs: null,
57
- attempts: 0,
58
- poolWaitAttempts: 0,
59
- __scrollBound: false,
19
+ const insertingIds = new Set(), state = {
20
+ pageKey: null,
21
+ cfg: null,
22
+ cfgPromise: null,
23
+
24
+ poolTopics: [],
25
+ poolPosts: [],
26
+ poolCategories: [],
27
+
28
+ usedTopics: new Set(),
29
+ usedPosts: new Set(),
30
+ usedCategories: new Set(),
31
+
32
+ // wrappers currently on page (FIFO for recycling)
33
+ liveBetween: [],
34
+ liveMessage: [],
35
+ liveCategory: [],
36
+
37
+ lastShowById: new Map(),
38
+ pendingById: new Set(),
39
+ definedIds: new Set(),
40
+
41
+ scheduled: false,
42
+ timer: null,
43
+
44
+ obs: null,
45
+ lastScrollRun: 0,
46
+ __scrollBound: false,
60
47
  };
61
48
 
62
49
  function normalizeBool(v) {
63
- return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
50
+ return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
64
51
  }
65
52
 
66
53
  function uniqInts(lines) {
67
- const out = [];
68
- const seen = new Set();
69
- for (const v of lines) {
70
- const n = parseInt(v, 10);
71
- if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
72
- seen.add(n);
73
- out.push(n);
74
- }
75
- }
76
- return out;
54
+ const out = [], seen = new Set();
55
+ for (const v of lines) {
56
+ const n = parseInt(v, 10);
57
+ if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
58
+ seen.add(n);
59
+ out.push(n);
60
+ }
61
+ }
62
+ return out;
77
63
  }
78
64
 
79
65
  function parsePool(raw) {
80
- if (!raw) return [];
81
- const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
82
- return uniqInts(lines);
66
+ if (!raw) return [];
67
+ const lines = String(raw).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
68
+ return uniqInts(lines);
83
69
  }
84
70
 
85
71
  function getPageKey() {
86
- try {
87
- const ax = window.ajaxify;
88
- if (ax && ax.data) {
89
- if (ax.data.tid) return `topic:${ax.data.tid}`;
90
- if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
91
- }
92
- } catch (e) {}
93
- return window.location.pathname;
72
+ try {
73
+ const ax = window.ajaxify;
74
+ if (ax && ax.data) {
75
+ if (ax.data.tid) return `topic:${ax.data.tid}`;
76
+ if (ax.data.cid) return `cid:${ax.data.cid}:${window.location.pathname}`;
77
+ }
78
+ } catch (e) {}
79
+ return window.location.pathname;
94
80
  }
95
81
 
96
82
  function getKind() {
97
- const p = window.location.pathname || '';
98
- if (/^\/topic\//.test(p)) return 'topic';
99
- if (/^\/category\//.test(p)) return 'categoryTopics';
100
- if (p === '/' || /^\/categories/.test(p)) return 'categories';
101
- // fallback by DOM
102
- if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
103
- if (document.querySelector(SELECTORS.postItem)) return 'topic';
104
- if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
105
- return 'other';
83
+ const p = window.location.pathname || '';
84
+ if (/^\/topic\//.test(p)) return 'topic';
85
+ if (/^\/category\//.test(p)) return 'categoryTopics';
86
+ if (p === '/' || /^\/categories/.test(p)) return 'categories';
87
+ // fallback by DOM
88
+ if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
89
+ if (document.querySelector(SELECTORS.postItem)) return 'topic';
90
+ if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
91
+ return 'other';
106
92
  }
107
93
 
108
94
  function getTopicItems() {
109
- return Array.from(document.querySelectorAll(SELECTORS.topicItem));
95
+ return Array.from(document.querySelectorAll(SELECTORS.topicItem));
110
96
  }
111
97
 
112
98
  function getCategoryItems() {
113
- return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
99
+ return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
114
100
  }
115
101
 
116
102
  function getPostContainers() {
117
- const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
118
- return nodes.filter((el) => {
119
- if (!el || !el.isConnected) return false;
120
- if (!el.querySelector('[component="post/content"]')) return false;
121
- const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
122
- if (parentPost && parentPost !== el) return false;
123
- if (el.getAttribute('component') === 'post/parent') return false;
124
- return true;
125
- });
103
+ const nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
104
+ return nodes.filter((el) => {
105
+ if (!el || !el.isConnected) return false;
106
+ if (!el.querySelector('[component="post/content"]')) return false;
107
+ const parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
108
+ if (parentPost && parentPost !== el) return false;
109
+ if (el.getAttribute('component') === 'post/parent') return false;
110
+ return true;
111
+ });
126
112
  }
127
113
 
128
-
129
114
  function safeRect(el) {
130
- try { return el.getBoundingClientRect(); } catch (e) { return null; }
115
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
131
116
  }
132
117
 
133
118
  function destroyPlaceholderIds(ids) {
134
- if (!ids || !ids.length) return;
135
- // Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
136
- const filtered = ids.filter((id) => {
137
- // Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
138
- try { return sessionDefinedIds.has(id); } catch (e) { return true; }
139
- });
140
- if (!filtered.length) return;
141
-
142
- const call = () => {
143
- try {
144
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
145
- window.ezstandalone.destroyPlaceholders(filtered);
146
- }
147
- } catch (e) {}
148
- };
149
- try {
150
- window.ezstandalone = window.ezstandalone || {};
151
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
152
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
153
- else window.ezstandalone.cmd.push(call);
154
- } catch (e) {}
119
+ if (!ids || !ids.length) return;
120
+ // Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
121
+ const filtered = ids.filter((id) => {
122
+ // Utiliser sessionDefinedIds (survit aux navigations) plutôt que state.definedIds
123
+ try { return sessionDefinedIds.has(id); } catch (e) { return true; }
124
+ });
125
+ if (!filtered.length) return;
126
+
127
+ const call = () => {
128
+ try {
129
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
130
+ window.ezstandalone.destroyPlaceholders(filtered);
131
+ }
132
+ } catch (e) {}
133
+ };
134
+ try {
135
+ window.ezstandalone = window.ezstandalone || {};
136
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
137
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
138
+ else window.ezstandalone.cmd.push(call);
139
+ } catch (e) {}
155
140
  }
156
141
 
157
142
  function getRecyclable(liveArr) {
158
- const margin = 600;
159
- for (let i = 0; i < liveArr.length; i++) {
160
- const entry = liveArr[i];
161
- if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
162
- const r = safeRect(entry.wrap);
163
- if (r && r.bottom < -margin) {
164
- liveArr.splice(i, 1);
165
- return entry;
166
- }
167
- }
168
- return null;
143
+ const margin = 600;
144
+ for (let i = 0; i < liveArr.length; i++) {
145
+ const entry = liveArr[i];
146
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
147
+ const r = safeRect(entry.wrap);
148
+ if (r && r.bottom < -margin) {
149
+ liveArr.splice(i, 1);
150
+ return entry;
151
+ }
152
+ }
153
+ return null;
169
154
  }
170
155
 
171
156
  function pickId(pool, liveArr) {
172
- if (pool.length) return { id: pool.shift(), recycled: null };
173
- const recycled = getRecyclable(liveArr);
174
- if (recycled) return { id: recycled.id, recycled };
175
- return { id: null, recycled: null };
157
+ if (pool.length) return { id: pool.shift(), recycled: null };
158
+ const recycled = getRecyclable(liveArr);
159
+ if (recycled) return { id: recycled.id, recycled };
160
+ return { id: null, recycled: null };
176
161
  }
177
162
 
178
-
179
163
  function resetPlaceholderInWrap(wrap, id) {
180
- try {
181
- if (!wrap) return;
182
- try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
183
- try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
184
- const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
185
- if (old) old.remove();
186
- // Remove any leftover markup inside wrapper
187
- wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
188
- const ph = document.createElement('div');
189
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
190
- wrap.appendChild(ph);
191
- } catch (e) {}
164
+ try {
165
+ if (!wrap) return;
166
+ try { wrap.removeAttribute('data-ezoic-filled'); } catch (e) {}
167
+ try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
168
+ const old = wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
169
+ if (old) old.remove();
170
+ // Remove any leftover markup inside wrapper
171
+ wrap.querySelectorAll && wrap.querySelectorAll('iframe, ins').forEach(n => n.remove());
172
+ const ph = document.createElement('div');
173
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
174
+ wrap.appendChild(ph);
175
+ } catch (e) {}
192
176
  }
193
177
 
194
178
  function isAdjacentAd(target) {
195
- if (!target || !target.nextElementSibling) return false;
196
- const next = target.nextElementSibling;
197
- if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
198
- return false;
179
+ if (!target || !target.nextElementSibling) return false;
180
+ const next = target.nextElementSibling;
181
+ if (next && next.classList && next.classList.contains(WRAP_CLASS)) return true;
182
+ return false;
199
183
  }
200
184
 
201
185
  function isPrevAd(target) {
202
- if (!target || !target.previousElementSibling) return false;
203
- const prev = target.previousElementSibling;
204
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
205
- return false;
186
+ if (!target || !target.previousElementSibling) return false;
187
+ const prev = target.previousElementSibling;
188
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) return true;
189
+ return false;
206
190
  }
207
191
 
208
192
  function buildWrap(id, kindClass, afterPos) {
209
- const wrap = document.createElement('div');
210
- wrap.className = `${WRAP_CLASS} ${kindClass}`;
211
- wrap.setAttribute('data-ezoic-after', String(afterPos));
212
- wrap.style.width = '100%';
193
+ const wrap = document.createElement('div');
194
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
195
+ wrap.setAttribute('data-ezoic-after', String(afterPos));
196
+ wrap.style.width = '100%';
213
197
 
214
- const ph = document.createElement('div');
215
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
216
- wrap.appendChild(ph);
217
- return wrap;
198
+ const ph = document.createElement('div');
199
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
200
+ wrap.appendChild(ph);
201
+ return wrap;
218
202
  }
219
203
 
220
204
  function findWrap(kindClass, afterPos) {
221
- return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
205
+ return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
222
206
  }
223
207
 
224
208
  function insertAfter(target, id, kindClass, afterPos) {
225
- if (!target || !target.insertAdjacentElement) return null;
226
- if (findWrap(kindClass, afterPos)) return null;
227
-
228
- // CRITICAL: Double-lock pour éviter race conditions sur les doublons
229
- // 1. Vérifier qu'aucun autre thread n'est en train d'insérer cet ID
230
- if (insertingIds.has(id)) return null;
231
-
232
- // 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
233
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
234
- if (existingPh && existingPh.isConnected) return null;
235
-
236
- // Acquérir le lock
237
- insertingIds.add(id);
238
-
239
- try {
240
- const wrap = buildWrap(id, kindClass, afterPos);
241
- target.insertAdjacentElement('afterend', wrap);
242
- attachFillObserver(wrap, id);
243
- return wrap;
244
- } finally {
245
- // Libérer le lock après 100ms (le temps que le DOM soit stable)
246
- setTimeout(() => insertingIds.delete(id), 100);
247
- }
209
+ if (!target || !target.insertAdjacentElement) return null;
210
+ if (findWrap(kindClass, afterPos)) return null;
211
+
212
+ // CRITICAL: Double-lock pour éviter race conditions sur les doublons
213
+ // 1. Vérifier qu'aucun autre thread n'est en train d'insérer cet ID
214
+ if (insertingIds.has(id)) return null;
215
+
216
+ // 2. Vérifier qu'aucun placeholder avec cet ID n'existe déjà dans le DOM
217
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
218
+ if (existingPh && existingPh.isConnected) return null;
219
+
220
+ // Acquérir le lock
221
+ insertingIds.add(id);
222
+
223
+ try {
224
+ const wrap = buildWrap(id, kindClass, afterPos);
225
+ target.insertAdjacentElement('afterend', wrap);
226
+ attachFillObserver(wrap, id);
227
+ return wrap;
228
+ } finally {
229
+ // Libérer le lock après 100ms (le temps que le DOM soit stable)
230
+ setTimeout(() => insertingIds.delete(id), 100);
231
+ }
248
232
  }
249
233
 
250
234
  function destroyUsedPlaceholders() {
251
- const ids = [];
252
- try {
253
- state.usedTopics.forEach((id) => ids.push(id));
254
- state.usedPosts.forEach((id) => ids.push(id));
255
- state.usedCategories.forEach((id) => ids.push(id));
256
- } catch (e) {}
257
- destroyPlaceholderIds(ids);
235
+ const ids = [...state.usedTopics, ...state.usedPosts, ...state.usedCategories];
236
+ if (ids.length) destroyPlaceholderIds(ids);
258
237
  }
259
238
 
260
239
  function patchShowAds() {
261
- // Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
262
- // Also ensures the patch is applied even if Ezoic loads after our script.
263
- const applyPatch = () => {
264
- try {
265
- window.ezstandalone = window.ezstandalone || {};
266
- const ez = window.ezstandalone;
267
- if (window.__nodebbEzoicPatched) return;
268
- if (typeof ez.showAds !== 'function') return;
269
-
270
- window.__nodebbEzoicPatched = true;
271
- const orig = ez.showAds;
272
-
273
- ez.showAds = function (arg) {
274
- if (Array.isArray(arg)) {
275
- const seen = new Set();
276
- for (const v of arg) {
277
- const id = parseInt(v, 10);
278
- if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
279
- seen.add(id);
280
- try { orig.call(ez, id); } catch (e) {}
281
- }
282
- return;
283
- }
284
- return orig.apply(ez, arguments);
285
- };
286
- } catch (e) {}
287
- };
288
-
289
- applyPatch();
290
- // Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
291
- if (!window.__nodebbEzoicPatched) {
292
- try {
293
- window.ezstandalone = window.ezstandalone || {};
294
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
295
- window.ezstandalone.cmd.push(applyPatch);
296
- } catch (e) {}
297
- }
298
- }
299
-
240
+ // Minimal safety net: batch showAds can be triggered by other scripts; split into individual calls.
241
+ // Also ensures the patch is applied even if Ezoic loads after our script.
242
+ const applyPatch = () => {
243
+ try {
244
+ window.ezstandalone = window.ezstandalone || {}, ez = window.ezstandalone;
245
+ if (window.__nodebbEzoicPatched) return;
246
+ if (typeof ez.showAds !== 'function') return;
247
+
248
+ window.__nodebbEzoicPatched = true;
249
+ const orig = ez.showAds;
250
+
251
+ ez.showAds = function (arg) {
252
+ if (Array.isArray(arg)) {
253
+ const seen = new Set();
254
+ for (const v of arg) {
255
+ const id = parseInt(v, 10);
256
+ if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
257
+ seen.add(id);
258
+ try { orig.call(ez, id); } catch (e) {}
259
+ }
260
+ return;
261
+ }
262
+ return orig.apply(ez, arguments);
263
+ };
264
+ } catch (e) {}
265
+ };
300
266
 
267
+ applyPatch();
268
+ // Si Ezoic n'est pas encore chargé, appliquer le patch via sa cmd queue
269
+ if (!window.__nodebbEzoicPatched) {
270
+ try {
271
+ window.ezstandalone = window.ezstandalone || {};
272
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
273
+ window.ezstandalone.cmd.push(applyPatch);
274
+ } catch (e) {}
275
+ }
276
+ }
301
277
 
302
278
  function markFilled(wrap) {
303
- try {
304
- if (!wrap) return;
305
- // Disconnect the fill observer first (no need to remove+re-add the attribute)
306
- try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
307
- wrap.setAttribute('data-ezoic-filled', '1');
308
- } catch (e) {}
279
+ try {
280
+ if (!wrap) return;
281
+ // Disconnect the fill observer first (no need to remove+re-add the attribute)
282
+ try { if (wrap.__ezoicFillObs) { wrap.__ezoicFillObs.disconnect(); wrap.__ezoicFillObs = null; } } catch (e) {}
283
+ wrap.setAttribute('data-ezoic-filled', '1');
284
+ } catch (e) {}
309
285
  }
310
286
 
311
287
  function isWrapMarkedFilled(wrap) {
312
- try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
288
+ try { return wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'; } catch (e) { return false; }
313
289
  }
314
290
 
315
291
  function attachFillObserver(wrap, id) {
316
- try {
317
- const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
318
- if (!ph) return;
319
- // Already filled?
320
- if (ph.childNodes && ph.childNodes.length > 0) {
321
- markFilled(wrap);
322
- state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id);
323
- return;
324
- }
325
- const obs = new MutationObserver(() => {
326
- if (ph.childNodes && ph.childNodes.length > 0) {
327
- markFilled(wrap);
328
- try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
329
- try { obs.disconnect(); } catch (e) {}
330
- }
331
- });
332
- obs.observe(ph, { childList: true, subtree: true });
333
- // Keep a weak reference on the wrapper so we can disconnect on recycle/remove
334
- wrap.__ezoicFillObs = obs;
335
- } catch (e) {}
292
+ try {
293
+ const ph = wrap && wrap.querySelector && wrap.querySelector(`#${PLACEHOLDER_PREFIX}${id}`);
294
+ if (!ph) return;
295
+ // Already filled?
296
+ if (ph.childNodes && ph.childNodes.length > 0) {
297
+ markFilled(wrap);
298
+ state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id);
299
+ return;
300
+ }
301
+ const obs = new MutationObserver(() => {
302
+ if (ph.childNodes && ph.childNodes.length > 0) {
303
+ markFilled(wrap);
304
+ try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
305
+ try { obs.disconnect(); } catch (e) {}
306
+ }
307
+ });
308
+ obs.observe(ph, { childList: true, subtree: true });
309
+ // Keep a weak reference on the wrapper so we can disconnect on recycle/remove
310
+ wrap.__ezoicFillObs = obs;
311
+ } catch (e) {}
336
312
  }
337
313
 
338
314
  function isPlaceholderFilled(id) {
339
- const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
340
- if (!ph || !ph.isConnected) return false;
341
-
342
- const wrap = ph.parentElement;
343
- if (wrap && isWrapMarkedFilled(wrap)) return true;
344
-
345
- const filled = !!(ph.childNodes && ph.childNodes.length > 0);
346
- if (filled) {
347
- try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
348
- try { markFilled(wrap); } catch (e) {}
349
- }
350
- return filled;
351
- }
352
-
353
- function scheduleRefill(delay = 350) {
354
- clearTimeout(state.retryTimer);
355
- state.retryTimer = setTimeout(refillUnfilled, delay);
356
- }
357
-
358
- function enqueueRetry(id) {
359
- if (!id) return;
360
- if (state.badIds && state.badIds.has(id)) return;
361
- if (state.retryQueueSet.has(id)) return;
362
- // Ne pas enqueue si showAds a été appelé récemment (< 2s) pour éviter "already been defined"
363
- const now = Date.now();
364
- const last = state.lastShowById.get(id) || 0;
365
- if (now - last < 2000) return;
366
- state.retryQueueSet.add(id);
367
- state.retryQueue.push(id);
368
- processRetryQueue();
369
- }
370
-
371
- function processRetryQueue() {
372
- if (state.retryQueueRunning) return;
373
- state.retryQueueRunning = true;
374
-
375
- const step = () => {
376
- const id = state.retryQueue.shift();
377
- if (!id) {
378
- state.retryQueueRunning = false;
379
- return;
380
- }
381
- state.retryQueueSet.delete(id);
382
- const attempts = (state.retryById.get(id) || 0);
383
- const phNow = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
384
- const wrapNow = phNow && phNow.parentElement;
385
- // Reset before re-requesting if Ezoic already defined this id but placeholder is now empty,
386
- // OR if a previous attempt already failed.
387
- if (wrapNow && wrapNow.isConnected && !isPlaceholderFilled(id) && !isWrapMarkedFilled(wrapNow) &&
388
- (state.definedIds.has(id) || attempts > 0)) {
389
- destroyPlaceholderIds([id]);
390
- resetPlaceholderInWrap(wrapNow, id);
391
- attachFillObserver(wrapNow, id);
392
- }
393
- // Ne pas appeler showAds si un autre appel est déjà en cours pour cet ID
394
- if (!state.pendingById.has(id)) {
395
- callShowAdsWhenReady(id);
396
- }
397
- setTimeout(step, 1100);
398
- };
399
-
400
- step();
401
- }
402
-
403
-
404
- function refillUnfilled() {
405
- // Si tous les IDs disponibles sont blacklistés, ne pas réessayer
406
- const totalPoolSize = state.poolTopics.length + state.poolPosts.length + state.poolCategories.length +
407
- state.usedTopics.size + state.usedPosts.size + state.usedCategories.size;
408
- if (totalPoolSize > 0 && state.badIds.size >= totalPoolSize) {
409
- // Tous les IDs disponibles sont blacklistés - arrêter les retry
410
- return;
411
- }
412
-
413
- const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
414
- let scheduledAny = false;
415
-
416
- for (const wrap of wraps) {
417
- // Skip les wrappers cachés par enforceNoAdjacentAds
418
- if (wrap.style && wrap.style.display === 'none') continue;
419
-
420
- const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
421
- if (!ph) continue;
422
- const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
423
- if (!Number.isFinite(id) || id <= 0) continue;
424
-
425
- if (isPlaceholderFilled(id)) {
426
- state.retryById.delete(id);
427
- continue;
428
- }
429
- // If wrapper was marked filled, don't try to refill even if placeholder temporarily appears empty.
430
- if (isWrapMarkedFilled(wrap)) {
431
- state.retryById.delete(id);
432
- continue;
433
- }
434
-
435
- // Skip si déjà blacklisté
436
- if (state.badIds && state.badIds.has(id)) {
437
- state.retryById.delete(id);
438
- continue;
439
- }
440
-
441
- const tries = (state.retryById.get(id) || 0);
442
- if (tries >= 3) {
443
- state.badIds && state.badIds.add(id);
444
- state.retryById.delete(id);
445
- continue;
446
- }
447
-
448
- const r = safeRect(wrap);
449
- if (r && (r.top > window.innerHeight + 1200 || r.bottom < -1200)) continue;
450
-
451
- // Incrémenter le compteur seulement si Ezoic est chargé
452
- // Sinon on est juste en attente de chargement, pas un vrai échec
453
- const ezoicReady = window.ezstandalone && typeof window.ezstandalone.showAds === 'function';
454
- if (ezoicReady) {
455
- state.retryById.set(id, tries + 1);
456
- }
457
- enqueueRetry(id);
458
- scheduledAny = true;
459
- }
460
-
461
- // Ne re-scheduler que si au moins un ID a été enqueued ET qu'il reste des IDs non-blacklistés
462
- if (scheduledAny) {
463
- const totalPoolSize = state.poolTopics.length + state.poolPosts.length + state.poolCategories.length +
464
- state.usedTopics.size + state.usedPosts.size + state.usedCategories.size;
465
- if (totalPoolSize === 0 || state.badIds.size < totalPoolSize) {
466
- scheduleRefill(700);
467
- }
468
- }
315
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
316
+ if (!ph || !ph.isConnected) return false;
317
+
318
+ const wrap = ph.parentElement;
319
+ if (wrap && isWrapMarkedFilled(wrap)) return true;
320
+
321
+ const filled = !!(ph.childNodes && ph.childNodes.length > 0);
322
+ if (filled) {
323
+ try { state.definedIds && state.definedIds.add(id); sessionDefinedIds.add(id); } catch (e) {}
324
+ try { markFilled(wrap); } catch (e) {}
325
+ }
326
+ return filled;
469
327
  }
470
328
 
471
329
  function callShowAdsWhenReady(id) {
472
- if (!id) return;
473
-
474
- const now = Date.now();
475
- const last = state.lastShowById.get(id) || 0;
476
- if (now - last < 3500) return;
477
-
478
- const phId = `${PLACEHOLDER_PREFIX}${id}`;
479
-
480
- const doCall = () => {
481
- try {
482
- window.ezstandalone = window.ezstandalone || {};
483
- if (typeof window.ezstandalone.showAds === 'function') {
484
- state.lastShowById.set(id, Date.now());
485
- window.ezstandalone.showAds(id);
486
- return true;
487
- }
488
- } catch (e) {}
489
- return false;
490
- };
491
-
492
- const startPageKey = state.pageKey;
493
- let attempts = 0;
494
- (function waitForPh() {
495
- // Abort if the user navigated away since this showAds was scheduled
496
- if (state.pageKey !== startPageKey) return;
497
- // Abort if another concurrent call is already handling this id
498
- if (state.pendingById.has(id)) return;
499
- attempts += 1;
500
- const el = document.getElementById(phId);
501
- if (el && el.isConnected) {
502
- // Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
503
- if (doCall()) {
504
- state.pendingById.delete(id); // nettoyage au cas où
505
- return;
506
- }
507
-
508
- // Ezoic n'est pas encore chargé → attendre via cmd queue
509
- state.pendingById.add(id);
510
-
511
- window.ezstandalone = window.ezstandalone || {};
512
- window.ezstandalone.cmd = window.ezstandalone.cmd || [];
513
- window.ezstandalone.cmd.push(() => {
514
- try {
515
- if (typeof window.ezstandalone.showAds === 'function') {
516
- state.pendingById.delete(id);
517
- state.lastShowById.set(id, Date.now());
518
- window.ezstandalone.showAds(id);
519
- }
520
- } catch (e) {}
521
- });
522
- // cmd.push suffit - pas besoin de tick() qui crée des doublons
523
- return;
524
- }
525
-
526
- if (attempts < 100) setTimeout(waitForPh, 50);
527
- })();
330
+ if (!id) return;
331
+
332
+ const now = Date.now(), last = state.lastShowById.get(id) || 0;
333
+ if (now - last < 3500) return;
334
+
335
+ const phId = `${PLACEHOLDER_PREFIX}${id}`, doCall = () => {
336
+ try {
337
+ window.ezstandalone = window.ezstandalone || {};
338
+ if (typeof window.ezstandalone.showAds === 'function') {
339
+ state.lastShowById.set(id, Date.now());
340
+ window.ezstandalone.showAds(id);
341
+ return true;
342
+ }
343
+ } catch (e) {}
344
+ return false;
345
+ };
346
+
347
+ const startPageKey = state.pageKey;
348
+ let attempts = 0;
349
+ (function waitForPh() {
350
+ // Abort if the user navigated away since this showAds was scheduled
351
+ if (state.pageKey !== startPageKey) return;
352
+ // Abort if another concurrent call is already handling this id
353
+ if (state.pendingById.has(id)) return;
354
+ attempts += 1;
355
+ const el = document.getElementById(phId);
356
+ if (el && el.isConnected) {
357
+ // Si doCall() réussit, Ezoic est chargé et showAds a été appelé → sortir
358
+ if (doCall()) {
359
+ state.pendingById.delete(id); // nettoyage au cas où
360
+ return;
361
+ }
362
+
363
+ // Ezoic n'est pas encore chargé → attendre via cmd queue
364
+ state.pendingById.add(id);
365
+
366
+ window.ezstandalone = window.ezstandalone || {};
367
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
368
+ window.ezstandalone.cmd.push(() => {
369
+ try {
370
+ if (typeof window.ezstandalone.showAds === 'function') {
371
+ state.pendingById.delete(id);
372
+ state.lastShowById.set(id, Date.now());
373
+ window.ezstandalone.showAds(id);
374
+ }
375
+ } catch (e) {}
376
+ });
377
+ // cmd.push suffit - pas besoin de tick() qui crée des doublons
378
+ return;
379
+ }
380
+
381
+ if (attempts < 100) setTimeout(waitForPh, 50);
382
+ })();
528
383
  }
529
384
 
530
385
  async function fetchConfig() {
531
- if (state.cfg) return state.cfg;
532
- if (state.cfgPromise) return state.cfgPromise;
533
-
534
- state.cfgPromise = (async () => {
535
- const MAX_TRIES = 3;
536
- let delay = 800;
537
- for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
538
- try {
539
- const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
540
- if (res.ok) {
541
- state.cfg = await res.json();
542
- return state.cfg;
543
- }
544
- } catch (e) {}
545
- if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
546
- delay *= 2;
547
- }
548
- return null;
549
- })();
550
-
551
- try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
386
+ if (state.cfg) return state.cfg;
387
+ if (state.cfgPromise) return state.cfgPromise;
388
+
389
+ state.cfgPromise = (async () => {
390
+ const MAX_TRIES = 3;
391
+ let delay = 800;
392
+ for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
393
+ try {
394
+ const res = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
395
+ if (res.ok) {
396
+ state.cfg = await res.json();
397
+ return state.cfg;
398
+ }
399
+ } catch (e) {}
400
+ if (attempt < MAX_TRIES) await new Promise(r => setTimeout(r, delay));
401
+ delay *= 2;
402
+ }
403
+ return null;
404
+ })();
405
+
406
+ try { return await state.cfgPromise; } finally { state.cfgPromise = null; }
552
407
  }
553
408
 
554
409
  function initPools(cfg) {
555
- if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
556
- if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
557
- if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
410
+ if (state.poolTopics.length === 0) state.poolTopics = parsePool(cfg.placeholderIds);
411
+ if (state.poolPosts.length === 0) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
412
+ if (state.poolCategories.length === 0) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
558
413
  }
559
414
 
560
415
  function computeTargets(count, interval, showFirst) {
561
- const out = [];
562
- if (count <= 0) return out;
563
- if (showFirst) out.push(1);
564
- for (let i = 1; i <= count; i++) {
565
- if (i % interval === 0) out.push(i);
566
- }
567
- return Array.from(new Set(out)).sort((a, b) => a - b);
416
+ const out = [];
417
+ if (count <= 0) return out;
418
+ if (showFirst) out.push(1);
419
+ for (let i = 1; i <= count; i++) {
420
+ if (i % interval === 0) out.push(i);
421
+ }
422
+ return Array.from(new Set(out)).sort((a, b) => a - b);
568
423
  }
569
424
 
570
425
  function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet) {
571
- if (!items.length) return 0;
572
- const targets = computeTargets(items.length, interval, showFirst);
573
-
574
- let inserted = 0;
575
- for (const afterPos of targets) {
576
- if (inserted >= MAX_INSERTS_PER_RUN) break;
577
-
578
- const el = items[afterPos - 1];
579
- if (!el || !el.isConnected) continue;
580
-
581
- // Prevent adjacent ads (DOM-based, robust against virtualization)
582
- if (isAdjacentAd(el) || isPrevAd(el)) {
583
- continue;
584
- }
585
-
586
- // Prevent back-to-back at load
587
- const prevWrap = findWrap(kindClass, afterPos - 1);
588
- if (prevWrap) continue;
589
-
590
- if (findWrap(kindClass, afterPos)) continue;
591
-
592
- const liveArr = (kindClass === 'ezoic-ad-between') ? state.liveBetween
593
- : (kindClass === 'ezoic-ad-message') ? state.liveMessage
594
- : state.liveCategory;
595
-
596
- const pick = pickId(kindPool, liveArr);
597
- const id = pick.id;
598
- if (!id) break;
599
-
600
- let wrap = null;
601
- if (pick.recycled && pick.recycled.wrap) {
602
- // Only destroy if Ezoic has actually defined this placeholder before
603
- if (sessionDefinedIds.has(id)) {
604
- destroyPlaceholderIds([id]);
605
- }
606
- // Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
607
- const oldWrap = pick.recycled.wrap;
608
- try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
609
- try { oldWrap && oldWrap.remove(); } catch (e) {}
610
- wrap = insertAfter(el, id, kindClass, afterPos);
611
- if (!wrap) continue;
612
- // Attendre 1500ms après destroy pour laisser Ezoic finir son cleanup interne
613
- setTimeout(() => { enqueueRetry(id); }, 1500);
614
- } else {
615
- usedSet.add(id);
616
- wrap = insertAfter(el, id, kindClass, afterPos);
617
- if (!wrap) continue;
618
- }
619
-
620
- liveArr.push({ id, wrap });
621
- // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
622
- if (wrap && (
623
- (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) ||
624
- (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
625
- )) {
626
- try { wrap.remove(); } catch (e) {}
627
- // Put id back if it was newly consumed (not recycled)
628
- if (!(pick.recycled && pick.recycled.wrap)) {
629
- try { kindPool.unshift(id); } catch (e) {}
630
- try { usedSet.delete(id); } catch (e) {}
631
- }
632
- continue;
633
- }
634
- if (!(pick.recycled && pick.recycled.wrap)) {
635
- // Ne pas appeler showAds si cet ID a déjà été blacklisté
636
- if (!state.badIds.has(id)) {
637
- callShowAdsWhenReady(id);
638
- }
639
- }
640
- inserted += 1;
641
- }
642
- return inserted;
426
+ if (!items.length) return 0;
427
+ const targets = computeTargets(items.length, interval, showFirst);
428
+
429
+ let inserted = 0;
430
+ for (const afterPos of targets) {
431
+ if (inserted >= MAX_INSERTS_PER_RUN) break;
432
+
433
+ const el = items[afterPos - 1];
434
+ if (!el || !el.isConnected) continue;
435
+
436
+ // Prevent adjacent ads (DOM-based, robust against virtualization)
437
+ if (isAdjacentAd(el) || isPrevAd(el)) {
438
+ continue;
439
+ }
440
+
441
+ // Prevent back-to-back at load
442
+ const prevWrap = findWrap(kindClass, afterPos - 1);
443
+ if (prevWrap) continue;
444
+
445
+ if (findWrap(kindClass, afterPos)) continue;
446
+
447
+ const liveArr = (kindClass === 'ezoic-ad-between') ? state.liveBetween
448
+ : (kindClass === 'ezoic-ad-message') ? state.liveMessage
449
+ : state.liveCategory, pick = pickId(kindPool, liveArr);
450
+ const id = pick.id;
451
+ if (!id) break;
452
+
453
+ let wrap = null;
454
+ if (pick.recycled && pick.recycled.wrap) {
455
+ // Only destroy if Ezoic has actually defined this placeholder before
456
+ if (sessionDefinedIds.has(id)) {
457
+ destroyPlaceholderIds([id]);
458
+ }
459
+ // Remove the old wrapper entirely, then create a fresh wrapper at the new position (same id)
460
+ const oldWrap = pick.recycled.wrap;
461
+ try { if (oldWrap && oldWrap.__ezoicFillObs) { oldWrap.__ezoicFillObs.disconnect(); } } catch (e) {}
462
+ try { oldWrap && oldWrap.remove(); } catch (e) {}
463
+ wrap = insertAfter(el, id, kindClass, afterPos);
464
+ if (!wrap) continue;
465
+ // Attendre que le wrapper soit dans le DOM puis appeler showAds
466
+ setTimeout(() => {
467
+ if (!false) {
468
+ callShowAdsWhenReady(id);
469
+ }
470
+ }, 1500);
471
+ } else {
472
+ usedSet.add(id);
473
+ wrap = insertAfter(el, id, kindClass, afterPos);
474
+ if (!wrap) continue;
475
+ }
476
+
477
+ liveArr.push({ id, wrap });
478
+ // If adjacency ended up happening (e.g. DOM shifts), rollback this placement.
479
+ if (wrap && (
480
+ (wrap.previousElementSibling && wrap.previousElementSibling.classList && wrap.previousElementSibling.classList.contains(WRAP_CLASS)) || (wrap.nextElementSibling && wrap.nextElementSibling.classList && wrap.nextElementSibling.classList.contains(WRAP_CLASS))
481
+ )) {
482
+ try { wrap.remove(); } catch (e) {}
483
+ // Put id back if it was newly consumed (not recycled)
484
+ if (!(pick.recycled && pick.recycled.wrap)) {
485
+ try { kindPool.unshift(id); } catch (e) {}
486
+ usedSet.delete(id);
487
+ }
488
+ continue;
489
+ }
490
+ if (!(pick.recycled && pick.recycled.wrap)) {
491
+ // Ne pas appeler showAds si cet ID a déjà été blacklisté
492
+ if (!false) {
493
+ callShowAdsWhenReady(id);
494
+ }
495
+ }
496
+ inserted += 1;
497
+ }
498
+ return inserted;
643
499
  }
644
500
 
645
501
  function enforceNoAdjacentAds() {
646
- const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
647
- for (let i = 0; i < ads.length; i++) {
648
- const ad = ads[i];
649
- const prev = ad.previousElementSibling;
650
- if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
651
- // Supprimer le wrapper adjacent au lieu de le cacher
652
- try {
653
- const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
654
- if (ph) {
655
- const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
656
- if (Number.isFinite(id) && id > 0) {
657
- // Détruire le placeholder si Ezoic l'a déjà défini
658
- if (sessionDefinedIds.has(id)) {
659
- destroyPlaceholderIds([id]);
660
- }
661
- }
662
- }
663
- ad.remove();
664
- } catch (e) {}
665
- }
666
- }
502
+ const ads = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
503
+ for (let i = 0; i < ads.length; i++) {
504
+ const ad = ads[i], prev = ad.previousElementSibling;
505
+ if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
506
+ // Supprimer le wrapper adjacent au lieu de le cacher
507
+ try {
508
+ const ph = ad.querySelector && ad.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
509
+ if (ph) {
510
+ const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
511
+ if (Number.isFinite(id) && id > 0) {
512
+ // Détruire le placeholder si Ezoic l'a déjà défini
513
+ if (sessionDefinedIds.has(id)) {
514
+ destroyPlaceholderIds([id]);
515
+ }
516
+ }
517
+ }
518
+ ad.remove();
519
+ } catch (e) {}
520
+ }
521
+ }
667
522
  }
668
523
 
669
524
  function cleanup() {
670
- destroyUsedPlaceholders();
671
-
672
- state.pageKey = getPageKey();
673
- state.cfg = null;
674
- state.cfgPromise = null;
675
-
676
- state.poolTopics = [];
677
- state.poolPosts = [];
678
- state.poolCategories = [];
679
- state.usedTopics.clear();
680
- state.usedPosts.clear();
681
- state.usedCategories.clear();
682
- state.liveBetween = [];
683
- state.liveMessage = [];
684
- state.liveCategory = [];
685
-
686
- state.lastShowById = new Map();
687
- state.pendingById = new Set();
688
- state.retryById = new Map();
689
- state.retryQueue = [];
690
- state.retryQueueSet = new Set();
691
- state.retryQueueRunning = false;
692
- state.badIds = new Set();
693
- state.definedIds = new Set();
694
-
695
- state.attempts = 0;
696
- state.poolWaitAttempts = 0;
697
-
698
- document.querySelectorAll(`.${WRAP_CLASS}`).forEach(el => el.remove());
699
-
700
- if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; }
701
- state.scheduled = false;
702
- clearTimeout(state.timer);
703
- state.timer = null;
704
- clearTimeout(state.retryTimer);
705
- state.retryTimer = null;
525
+ destroyUsedPlaceholders();
526
+
527
+ state.pageKey = getPageKey();
528
+ state.cfg = null;
529
+ state.cfgPromise = null;
530
+
531
+ state.poolTopics = [];
532
+ state.poolPosts = [];
533
+ state.poolCategories = [];
534
+ state.usedTopics.clear();
535
+ state.usedPosts.clear();
536
+ state.usedCategories.clear();
537
+ state.liveBetween = [];
538
+ state.liveMessage = [];
539
+ state.liveCategory = [];
540
+
541
+ state.lastShowById = new Map();
542
+ state.pendingById = new Set();
543
+ state.definedIds = new Set();
544
+
545
+ document.querySelectorAll(`.${WRAP_CLASS}`).forEach(el => el.remove());
546
+
547
+ if (state.obs) { try { state.obs.disconnect(); } catch (e) {} state.obs = null; }
548
+ state.scheduled = false;
549
+ clearTimeout(state.timer);
550
+ state.timer = null;
706
551
  }
707
552
 
708
553
  function ensureObserver() {
709
- if (state.obs) return;
710
- state.obs = new MutationObserver(() => scheduleRun('mutation'));
711
- try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
554
+ if (state.obs) return;
555
+ state.obs = new MutationObserver(() => scheduleRun('mutation'));
556
+ try { state.obs.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
712
557
  }
713
558
 
714
559
  async function runCore() {
715
- patchShowAds();
716
-
717
- const cfg = await fetchConfig();
718
- if (!cfg || cfg.excluded) return;
719
-
720
- initPools(cfg);
721
-
722
- const kind = getKind();
723
- let inserted = 0;
724
-
725
- if (kind === 'topic') {
726
- if (normalizeBool(cfg.enableMessageAds)) {
727
- inserted = injectBetween('ezoic-ad-message', getPostContainers(),
728
- Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
729
- normalizeBool(cfg.showFirstMessageAd),
730
- state.poolPosts,
731
- state.usedPosts);
732
- }
733
- } else if (kind === 'categoryTopics') {
734
- if (normalizeBool(cfg.enableBetweenAds)) {
735
- inserted = injectBetween('ezoic-ad-between', getTopicItems(),
736
- Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
737
- normalizeBool(cfg.showFirstTopicAd),
738
- state.poolTopics,
739
- state.usedTopics);
740
- }
741
- } else if (kind === 'categories') {
742
- if (normalizeBool(cfg.enableCategoryAds)) {
743
- inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
744
- Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
745
- normalizeBool(cfg.showFirstCategoryAd),
746
- state.poolCategories,
747
- state.usedCategories);
748
- }
749
- }
750
-
751
- enforceNoAdjacentAds();
752
- scheduleRefill(250);
753
-
754
- // If nothing inserted and list isn't in DOM yet (first click), retry a bit
755
- let count = 0;
756
- if (kind === 'topic') count = getPostContainers().length;
757
- else if (kind === 'categoryTopics') count = getTopicItems().length;
758
- else if (kind === 'categories') count = getCategoryItems().length;
759
-
760
- if (count === 0 && state.attempts < 25) {
761
- state.attempts += 1;
762
- setTimeout(() => scheduleRun('await-items'), 120);
763
- return;
764
- }
765
-
766
- if (inserted >= MAX_INSERTS_PER_RUN) {
767
- // Plus d'insertions possibles ce cycle, continuer immédiatement
768
- setTimeout(() => scheduleRun('continue'), 140);
769
- } else if (inserted === 0 && count > 0) {
770
- // Pool épuisé ou recyclage pas encore disponible.
771
- // Réessayer jusqu'à 8 fois (toutes les 400ms) pour laisser aux anciens wrappers
772
- // le temps de défiler hors écran et devenir recyclables.
773
- if (state.poolWaitAttempts < 8) {
774
- state.poolWaitAttempts += 1;
775
- setTimeout(() => scheduleRun('pool-wait'), 400);
776
- } else {
777
- state.poolWaitAttempts = 0;
778
- }
779
- } else if (inserted > 0) {
780
- state.poolWaitAttempts = 0;
781
- }
560
+ patchShowAds();
561
+
562
+ const cfg = await fetchConfig();
563
+ if (!cfg || cfg.excluded) return;
564
+
565
+ initPools(cfg);
566
+
567
+ const kind = getKind();
568
+ let inserted = 0;
569
+
570
+ if (kind === 'topic') {
571
+ if (normalizeBool(cfg.enableMessageAds)) {
572
+ inserted = injectBetween('ezoic-ad-message', getPostContainers(),
573
+ Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
574
+ normalizeBool(cfg.showFirstMessageAd),
575
+ state.poolPosts,
576
+ state.usedPosts);
577
+ }
578
+ } else if (kind === 'categoryTopics') {
579
+ if (normalizeBool(cfg.enableBetweenAds)) {
580
+ inserted = injectBetween('ezoic-ad-between', getTopicItems(),
581
+ Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
582
+ normalizeBool(cfg.showFirstTopicAd),
583
+ state.poolTopics,
584
+ state.usedTopics);
585
+ }
586
+ } else if (kind === 'categories') {
587
+ if (normalizeBool(cfg.enableCategoryAds)) {
588
+ inserted = injectBetween('ezoic-ad-categories', getCategoryItems(),
589
+ Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
590
+ normalizeBool(cfg.showFirstCategoryAd),
591
+ state.poolCategories,
592
+ state.usedCategories);
593
+ }
594
+ }
595
+
596
+ enforceNoAdjacentAds();
597
+
598
+ // If nothing inserted and list isn't in DOM yet (first click), retry a bit
599
+ let count = 0;
600
+ if (kind === 'topic') count = getPostContainers().length;
601
+ else if (kind === 'categoryTopics') count = getTopicItems().length;
602
+ else if (kind === 'categories') count = getCategoryItems().length;
603
+
604
+ if (count === 0 && 0 < 25) {
605
+ setTimeout(() => scheduleRun('await-items'), 120);
606
+ return;
607
+ }
608
+
609
+ if (inserted >= MAX_INSERTS_PER_RUN) {
610
+ // Plus d'insertions possibles ce cycle, continuer immédiatement
611
+ setTimeout(() => scheduleRun('continue'), 140);
612
+ } else if (inserted === 0 && count > 0) {
613
+ // Pool épuisé ou recyclage pas encore disponible.
614
+ // Réessayer jusqu'à 8 fois (toutes les 400ms) pour laisser aux anciens wrappers
615
+ // le temps de défiler hors écran et devenir recyclables.
616
+ if (state.poolWaitAttempts < 8) {
617
+ state.poolWaitAttempts += 1;
618
+ setTimeout(() => scheduleRun('pool-wait'), 400);
619
+ } else {
620
+ }
621
+ } else if (inserted > 0) {
622
+ }
782
623
  }
783
624
 
784
625
  function scheduleRun() {
785
- if (state.scheduled) return;
786
- state.scheduled = true;
626
+ if (state.scheduled) return;
627
+ state.scheduled = true;
787
628
 
788
- clearTimeout(state.timer);
789
- state.timer = setTimeout(() => {
790
- state.scheduled = false;
791
- const pk = getPageKey();
792
- if (state.pageKey && pk !== state.pageKey) return;
793
- runCore().catch(() => {});
794
- }, 80);
629
+ clearTimeout(state.timer);
630
+ state.timer = setTimeout(() => {
631
+ state.scheduled = false;
632
+ const pk = getPageKey();
633
+ if (state.pageKey && pk !== state.pageKey) return;
634
+ runCore().catch(() => {});
635
+ }, 80);
795
636
  }
796
637
 
797
638
  function bind() {
798
- if (!$) return;
639
+ if (!$) return;
799
640
 
800
- $(window).off('.ezoicInfinite');
641
+ $(window).off('.ezoicInfinite');
801
642
 
802
- $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
643
+ $(window).on('action:ajaxify.start.ezoicInfinite', () => cleanup());
803
644
 
804
- $(window).on('action:ajaxify.end.ezoicInfinite', () => {
805
- state.pageKey = getPageKey();
806
- ensureObserver();
807
- // Attendre que le DOM soit stable ET que Ezoic se charge
808
- setTimeout(scheduleRun, 500);
809
- setTimeout(scheduleRun, 1200);
810
- });
645
+ $(window).on('action:ajaxify.end.ezoicInfinite', () => {
646
+ state.pageKey = getPageKey();
647
+ ensureObserver();
648
+ // CRITIQUE : Après navigation ajaxify, Ezoic est déjà chargé mais ne sait pas
649
+ // qu'on va insérer de nouveaux placeholders. Il faut le forcer à re-scanner.
650
+ try {
651
+ if (window.ezstandalone && typeof window.ezstandalone.enable === 'function') {
652
+ // enable() force Ezoic à re-scanner toute la page
653
+ window.ezstandalone.enable();
654
+ }
655
+ } catch (e) {
656
+
657
+ }
658
+ // Attendre que les nouveaux placeholders soient dans le DOM
659
+ setTimeout(scheduleRun, 500);
660
+ setTimeout(scheduleRun, 1000);
661
+ });
811
662
 
812
- $(window).on('action:category.loaded.ezoicInfinite', () => {
813
- ensureObserver();
814
- // category.loaded se déclenche après ajaxify.end, ne pas dupliquer
815
- setTimeout(scheduleRun, 100);
816
- });
663
+ $(window).on('action:category.loaded.ezoicInfinite', () => {
664
+ ensureObserver();
665
+ // category.loaded = infinite scroll, Ezoic déjà chargé normalement
666
+ setTimeout(scheduleRun, 300);
667
+ });
817
668
 
818
- $(window).on('action:topics.loaded.ezoicInfinite', () => {
819
- ensureObserver();
820
- setTimeout(scheduleRun, 100);
821
- });
669
+ $(window).on('action:topics.loaded.ezoicInfinite', () => {
670
+ ensureObserver();
671
+ setTimeout(scheduleRun, 300);
672
+ });
822
673
 
823
- $(window).on('action:topic.loaded.ezoicInfinite', () => {
824
- ensureObserver();
825
- setTimeout(scheduleRun, 100);
826
- });
674
+ $(window).on('action:topic.loaded.ezoicInfinite', () => {
675
+ ensureObserver();
676
+ setTimeout(scheduleRun, 300);
677
+ });
827
678
 
828
- $(window).on('action:posts.loaded.ezoicInfinite', () => {
829
- ensureObserver();
830
- // posts.loaded = infinite scroll, attendre un peu pour éviter trop de runs rapprochés
831
- setTimeout(scheduleRun, 200);
832
- });
679
+ $(window).on('action:posts.loaded.ezoicInfinite', () => {
680
+ ensureObserver();
681
+ // posts.loaded = infinite scroll
682
+ setTimeout(scheduleRun, 400);
683
+ });
833
684
  }
834
685
 
835
686
  function bindScroll() {
836
- if (state.__scrollBound) return;
837
- state.__scrollBound = true;
838
- let ticking = false;
839
- window.addEventListener('scroll', () => {
840
- if (ticking) return;
841
- ticking = true;
842
- window.requestAnimationFrame(() => {
843
- ticking = false;
844
- // Réinitialiser le compteur d'attente de recyclage à chaque reprise de scroll
845
- state.poolWaitAttempts = 0;
846
- enforceNoAdjacentAds();
847
- scheduleRefill(2000);
848
- // Tenter aussi d'insérer de nouvelles pubs si de nouveaux items sont apparus
849
- scheduleRun();
850
- });
851
- }, { passive: true });
687
+ if (state.__scrollBound) return;
688
+ state.__scrollBound = true;
689
+ let ticking = false;
690
+ window.addEventListener('scroll', () => {
691
+ if (ticking) return;
692
+ ticking = true;
693
+ window.requestAnimationFrame(() => {
694
+ ticking = false;
695
+ enforceNoAdjacentAds();
696
+ // Debounce scheduleRun - une fois toutes les 2 secondes max au scroll
697
+ const now = Date.now();
698
+ if (!state.lastScrollRun || now - state.lastScrollRun > 2000) {
699
+ state.lastScrollRun = now;
700
+ scheduleRun();
701
+ }
702
+ });
703
+ }, { passive: true });
704
+ }
705
+
706
+ // Fonction qui attend que Ezoic soit vraiment chargé
707
+ function waitForEzoicThenRun() {
708
+ let attempts = 0;
709
+ const maxAttempts = 50; // 50 × 200ms = 10s max
710
+
711
+ (function check() {
712
+ attempts++;
713
+ // Vérifier si Ezoic est chargé
714
+ if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
715
+ // Diagnostic : afficher les placeholders disponibles
716
+ try {
717
+ if (window.ezstandalone.placeholders) {
718
+ }
719
+ } catch (e) {}
720
+ // Ezoic est prêt → lancer l'insertion
721
+ scheduleRun();
722
+ setTimeout(scheduleRun, 300);
723
+ return;
724
+ }
725
+ // Ezoic pas encore prêt
726
+ if (attempts >= maxAttempts) {
727
+ // Tenter quand même
728
+ scheduleRun();
729
+ return;
730
+ }
731
+ // Réessayer dans 200ms
732
+ setTimeout(check, 200);
733
+ })();
852
734
  }
853
735
 
854
736
  cleanup();
@@ -856,7 +738,7 @@
856
738
  bindScroll();
857
739
  ensureObserver();
858
740
  state.pageKey = getPageKey();
859
- // Attendre un peu plus longtemps au démarrage initial pour Ezoic
860
- setTimeout(scheduleRun, 800);
861
- setTimeout(scheduleRun, 1500);
741
+
742
+ // Attendre que Ezoic soit chargé avant d'insérer
743
+ waitForEzoicThenRun();
862
744
  })();