nodebb-plugin-ezoic-infinite 1.8.7 → 1.8.8
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/package.json +1 -1
- package/public/client.js +359 -147
- package/public/style.css +9 -15
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -77,12 +77,18 @@
|
|
|
77
77
|
const A_CREATED = 'data-ezoic-created'; // timestamp création ms
|
|
78
78
|
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
// Tunables (stables en prod)
|
|
81
81
|
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
82
|
-
const MAX_INSERTS_RUN =
|
|
83
|
-
const MAX_INFLIGHT =
|
|
84
|
-
const
|
|
85
|
-
const
|
|
82
|
+
const MAX_INSERTS_RUN = 10; // plus réactif si NodeBB injecte en rafale
|
|
83
|
+
const MAX_INFLIGHT = 2; // ids max simultanés en vol (garde-fou)
|
|
84
|
+
const MAX_SHOW_BATCH = 4; // ids max par appel showAds(...ids)
|
|
85
|
+
const SHOW_THROTTLE_MS = 500; // anti-spam showAds() par id (plus réactif)
|
|
86
|
+
const SHOW_RELEASE_MS = 300; // relâche inflight après showAds() batché
|
|
87
|
+
const SHOW_FAILSAFE_MS = 7000; // relâche forcée si stack pub lente
|
|
88
|
+
const BATCH_FLUSH_MS = 30; // micro-buffer pour regrouper les ids proches
|
|
89
|
+
const MAX_DESTROY_BATCH = 4; // ids max par destroyPlaceholders(...ids)
|
|
90
|
+
const DESTROY_FLUSH_MS = 30; // micro-buffer destroy pour lisser les rafales
|
|
91
|
+
const BURST_COOLDOWN_MS = 100; // délai min entre deux déclenchements de burst
|
|
86
92
|
|
|
87
93
|
// Marges IO larges et fixes — observer créé une seule fois au boot
|
|
88
94
|
const IO_MARGIN_DESKTOP = '2500px 0px 2500px 0px';
|
|
@@ -126,14 +132,23 @@
|
|
|
126
132
|
inflight: 0, // showAds() en cours
|
|
127
133
|
pending: [], // ids en attente de slot inflight
|
|
128
134
|
pendingSet: new Set(),
|
|
135
|
+
showBatchTimer: 0,
|
|
136
|
+
destroyBatchTimer: 0,
|
|
137
|
+
destroyPending: [],
|
|
138
|
+
destroyPendingSet: new Set(),
|
|
139
|
+
sweepQueued: false,
|
|
129
140
|
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
141
|
+
ezActiveIds: new Set(), // ids actifs côté plugin (wrap présent / récemment show)
|
|
142
|
+
ezShownSinceDestroy: new Set(), // ids déjà show depuis le dernier destroy Ezoic
|
|
143
|
+
scrollDir: 1, // 1=bas, -1=haut
|
|
144
|
+
scrollSpeed: 0, // px/s approx (EMA)
|
|
145
|
+
lastScrollY: 0,
|
|
146
|
+
lastScrollTs: 0,
|
|
130
147
|
runQueued: false,
|
|
131
148
|
burstActive: false,
|
|
132
149
|
burstDeadline: 0,
|
|
133
150
|
burstCount: 0,
|
|
134
151
|
lastBurstTs: 0,
|
|
135
|
-
lastScrollY: 0,
|
|
136
|
-
scrollDir: 1, // 1=down, -1=up
|
|
137
152
|
};
|
|
138
153
|
|
|
139
154
|
let blockedUntil = 0;
|
|
@@ -144,10 +159,120 @@
|
|
|
144
159
|
const normBool = v => v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
145
160
|
const isFilled = n => !!(n?.querySelector?.('iframe, ins, img, video, [data-google-container-id]'));
|
|
146
161
|
|
|
162
|
+
function healFalseEmpty(root = document) {
|
|
163
|
+
try {
|
|
164
|
+
const list = [];
|
|
165
|
+
if (root instanceof Element && root.classList?.contains(WRAP_CLASS)) list.push(root);
|
|
166
|
+
const found = root.querySelectorAll ? root.querySelectorAll(`.${WRAP_CLASS}.is-empty`) : [];
|
|
167
|
+
for (const w of found) list.push(w);
|
|
168
|
+
for (const w of list) {
|
|
169
|
+
if (!w?.classList?.contains('is-empty')) continue;
|
|
170
|
+
if (isFilled(w)) w.classList.remove('is-empty');
|
|
171
|
+
}
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function phEl(id) {
|
|
176
|
+
return document.getElementById(`${PH_PREFIX}${id}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hasSinglePlaceholder(id) {
|
|
180
|
+
try { return document.querySelectorAll(`#${PH_PREFIX}${id}`).length === 1; } catch (_) { return false; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function canShowPlaceholderId(id, now = ts()) {
|
|
184
|
+
const n = parseInt(id, 10);
|
|
185
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
186
|
+
if (now - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return false;
|
|
187
|
+
const ph = phEl(n);
|
|
188
|
+
if (!ph?.isConnected || isFilled(ph)) return false;
|
|
189
|
+
if (!hasSinglePlaceholder(n)) return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function queueSweepDeadWraps() {
|
|
194
|
+
if (S.sweepQueued) return;
|
|
195
|
+
S.sweepQueued = true;
|
|
196
|
+
requestAnimationFrame(() => {
|
|
197
|
+
S.sweepQueued = false;
|
|
198
|
+
sweepDeadWraps();
|
|
199
|
+
healFalseEmpty();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getDynamicShowBatchMax() {
|
|
204
|
+
const speed = S.scrollSpeed || 0;
|
|
205
|
+
const pend = S.pending.length;
|
|
206
|
+
// Scroll très rapide => petits batches (réduit le churn/unused)
|
|
207
|
+
if (speed > 2600) return 2;
|
|
208
|
+
if (speed > 1400) return 3;
|
|
209
|
+
// Peu de candidats => flush plus vite, inutile d'attendre 4
|
|
210
|
+
if (pend <= 1) return 1;
|
|
211
|
+
if (pend <= 3) return 2;
|
|
212
|
+
// Par défaut compromis dynamique
|
|
213
|
+
return 3;
|
|
214
|
+
}
|
|
215
|
+
|
|
147
216
|
function mutate(fn) {
|
|
148
217
|
S.mutGuard++;
|
|
149
218
|
try { fn(); } finally { S.mutGuard--; }
|
|
150
219
|
}
|
|
220
|
+
function scheduleDestroyFlush() {
|
|
221
|
+
if (S.destroyBatchTimer) return;
|
|
222
|
+
S.destroyBatchTimer = setTimeout(() => {
|
|
223
|
+
S.destroyBatchTimer = 0;
|
|
224
|
+
flushDestroyBatch();
|
|
225
|
+
}, DESTROY_FLUSH_MS);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function flushDestroyBatch() {
|
|
229
|
+
if (!S.destroyPending.length) return;
|
|
230
|
+
const ids = [];
|
|
231
|
+
while (S.destroyPending.length && ids.length < MAX_DESTROY_BATCH) {
|
|
232
|
+
const id = S.destroyPending.shift();
|
|
233
|
+
S.destroyPendingSet.delete(id);
|
|
234
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
235
|
+
ids.push(id);
|
|
236
|
+
}
|
|
237
|
+
if (ids.length) {
|
|
238
|
+
try {
|
|
239
|
+
const ez = window.ezstandalone;
|
|
240
|
+
const run = () => { try { ez?.destroyPlaceholders?.(ids); } catch (_) {} };
|
|
241
|
+
try { (typeof ez?.cmd?.push === 'function') ? ez.cmd.push(run) : run(); } catch (_) {}
|
|
242
|
+
} catch (_) {}
|
|
243
|
+
}
|
|
244
|
+
if (S.destroyPending.length) scheduleDestroyFlush();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function destroyEzoicId(id) {
|
|
248
|
+
if (!Number.isFinite(id) || id <= 0) return;
|
|
249
|
+
if (!S.ezActiveIds.has(id)) return;
|
|
250
|
+
S.ezActiveIds.delete(id);
|
|
251
|
+
if (!S.destroyPendingSet.has(id)) {
|
|
252
|
+
S.destroyPending.push(id);
|
|
253
|
+
S.destroyPendingSet.add(id);
|
|
254
|
+
}
|
|
255
|
+
scheduleDestroyFlush();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function destroyBeforeReuse(ids) {
|
|
259
|
+
const out = [];
|
|
260
|
+
const toDestroy = [];
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
for (const raw of (ids || [])) {
|
|
263
|
+
const id = parseInt(raw, 10);
|
|
264
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
265
|
+
seen.add(id);
|
|
266
|
+
out.push(id);
|
|
267
|
+
if (S.ezShownSinceDestroy.has(id)) toDestroy.push(id);
|
|
268
|
+
}
|
|
269
|
+
if (toDestroy.length) {
|
|
270
|
+
try { window.ezstandalone?.destroyPlaceholders?.(toDestroy); } catch (_) {}
|
|
271
|
+
for (const id of toDestroy) S.ezShownSinceDestroy.delete(id);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
151
276
|
|
|
152
277
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
153
278
|
|
|
@@ -302,6 +427,25 @@
|
|
|
302
427
|
return null;
|
|
303
428
|
}
|
|
304
429
|
|
|
430
|
+
function sweepDeadWraps() {
|
|
431
|
+
// NodeBB peut retirer des nœuds sans passer par dropWrap() (virtualisation / rerender).
|
|
432
|
+
// On libère alors les IDs/keys fantômes pour éviter l'épuisement du pool.
|
|
433
|
+
for (const [key, wrap] of Array.from(S.wrapByKey.entries())) {
|
|
434
|
+
if (wrap?.isConnected) continue;
|
|
435
|
+
S.wrapByKey.delete(key);
|
|
436
|
+
const id = parseInt(wrap?.getAttribute?.(A_WRAPID), 10);
|
|
437
|
+
if (Number.isFinite(id)) {
|
|
438
|
+
S.mountedIds.delete(id);
|
|
439
|
+
S.pendingSet.delete(id);
|
|
440
|
+
S.lastShow.delete(id);
|
|
441
|
+
S.ezActiveIds.delete(id);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (S.pending.length) {
|
|
445
|
+
S.pending = S.pending.filter(id => S.pendingSet.has(id));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
305
449
|
/**
|
|
306
450
|
* Pool épuisé : recycle un wrap loin au-dessus du viewport.
|
|
307
451
|
* Séquence avec délais (destroyPlaceholders est asynchrone) :
|
|
@@ -309,83 +453,84 @@
|
|
|
309
453
|
* displayMore = API Ezoic prévue pour l'infinite scroll.
|
|
310
454
|
* Priorité : wraps vides d'abord, remplis si nécessaire.
|
|
311
455
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (rect.top > belowThreshold) {
|
|
348
|
-
if (!isFilled(wrap)) {
|
|
349
|
-
if (rect.top > belowEmptyTop) { belowEmptyTop = rect.top; belowEmpty = wrap; }
|
|
350
|
-
} else {
|
|
351
|
-
if (rect.top > belowFilledTop) { belowFilledTop = rect.top; belowFilled = wrap; }
|
|
352
|
-
}
|
|
456
|
+
function recycleAndMove(klass, targetEl, newKey) {
|
|
457
|
+
const ez = window.ezstandalone;
|
|
458
|
+
if (typeof ez?.destroyPlaceholders !== 'function' ||
|
|
459
|
+
typeof ez?.define !== 'function' ||
|
|
460
|
+
typeof ez?.displayMore !== 'function') return null;
|
|
461
|
+
|
|
462
|
+
const vh = window.innerHeight || 800;
|
|
463
|
+
const preferAbove = S.scrollDir >= 0; // scroll bas => recycle en haut
|
|
464
|
+
const farAbove = -vh;
|
|
465
|
+
const farBelow = vh * 2;
|
|
466
|
+
|
|
467
|
+
let bestPrefEmpty = null, bestPrefMetric = Infinity;
|
|
468
|
+
let bestPrefFilled = null, bestPrefFilledMetric = Infinity;
|
|
469
|
+
let bestAnyEmpty = null, bestAnyMetric = Infinity;
|
|
470
|
+
let bestAnyFilled = null, bestAnyFilledMetric = Infinity;
|
|
471
|
+
|
|
472
|
+
for (const wrap of S.wrapByKey.values()) {
|
|
473
|
+
if (!wrap?.isConnected || !wrap.classList?.contains(WRAP_CLASS) || !wrap.classList.contains(klass)) continue;
|
|
474
|
+
try {
|
|
475
|
+
const rect = wrap.getBoundingClientRect();
|
|
476
|
+
const isAbove = rect.bottom <= farAbove;
|
|
477
|
+
const isBelow = rect.top >= farBelow;
|
|
478
|
+
const anyFar = isAbove || isBelow;
|
|
479
|
+
if (!anyFar) continue;
|
|
480
|
+
|
|
481
|
+
const qualifies = preferAbove ? isAbove : isBelow;
|
|
482
|
+
const metric = preferAbove ? Math.abs(rect.bottom) : Math.abs(rect.top - vh);
|
|
483
|
+
const filled = isFilled(wrap);
|
|
484
|
+
|
|
485
|
+
if (qualifies) {
|
|
486
|
+
if (!filled) {
|
|
487
|
+
if (metric < bestPrefMetric) { bestPrefMetric = metric; bestPrefEmpty = wrap; }
|
|
488
|
+
} else {
|
|
489
|
+
if (metric < bestPrefFilledMetric) { bestPrefFilledMetric = metric; bestPrefFilled = wrap; }
|
|
353
490
|
}
|
|
354
|
-
}
|
|
355
|
-
|
|
491
|
+
}
|
|
492
|
+
if (!filled) {
|
|
493
|
+
if (metric < bestAnyMetric) { bestAnyMetric = metric; bestAnyEmpty = wrap; }
|
|
494
|
+
} else {
|
|
495
|
+
if (metric < bestAnyFilledMetric) { bestAnyFilledMetric = metric; bestAnyFilled = wrap; }
|
|
496
|
+
}
|
|
497
|
+
} catch (_) {}
|
|
498
|
+
}
|
|
356
499
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
500
|
+
const best = bestPrefEmpty ?? bestPrefFilled ?? bestAnyEmpty ?? bestAnyFilled;
|
|
501
|
+
if (!best) return null;
|
|
502
|
+
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
503
|
+
if (!Number.isFinite(id)) return null;
|
|
504
|
+
|
|
505
|
+
const oldKey = best.getAttribute(A_ANCHOR);
|
|
506
|
+
try { const ph = best.querySelector(`#${PH_PREFIX}${id}`); if (ph) S.io?.unobserve(ph); } catch (_) {}
|
|
507
|
+
mutate(() => {
|
|
508
|
+
best.setAttribute(A_ANCHOR, newKey);
|
|
509
|
+
best.setAttribute(A_CREATED, String(ts()));
|
|
510
|
+
best.setAttribute(A_SHOWN, '0');
|
|
511
|
+
best.classList.remove('is-empty');
|
|
512
|
+
const ph = best.querySelector(`#${PH_PREFIX}${id}`);
|
|
513
|
+
if (ph) ph.innerHTML = '';
|
|
514
|
+
targetEl.insertAdjacentElement('afterend', best);
|
|
515
|
+
});
|
|
516
|
+
if (oldKey && S.wrapByKey.get(oldKey) === best) S.wrapByKey.delete(oldKey);
|
|
517
|
+
S.wrapByKey.set(newKey, best);
|
|
518
|
+
|
|
519
|
+
const doDestroy = () => {
|
|
520
|
+
if (S.ezShownSinceDestroy.has(id)) {
|
|
521
|
+
try { ez.destroyPlaceholders([id]); } catch (_) {}
|
|
522
|
+
S.ezShownSinceDestroy.delete(id);
|
|
523
|
+
}
|
|
524
|
+
S.ezActiveIds.delete(id);
|
|
525
|
+
setTimeout(doDefine, 330);
|
|
526
|
+
};
|
|
527
|
+
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
528
|
+
const doDisplay = () => { try { ez.displayMore([id]); S.ezActiveIds.add(id); S.ezShownSinceDestroy.add(id); } catch (_) {} };
|
|
529
|
+
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
380
530
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
384
|
-
const doDisplay = () => { try { ez.displayMore([id]); } catch (_) {} };
|
|
385
|
-
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
531
|
+
return { id, wrap: best };
|
|
532
|
+
}
|
|
386
533
|
|
|
387
|
-
return { id, wrap: best };
|
|
388
|
-
}
|
|
389
534
|
|
|
390
535
|
// ── Wraps DOM — création / suppression ────────────────────────────────────
|
|
391
536
|
|
|
@@ -421,7 +566,7 @@
|
|
|
421
566
|
const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
|
|
422
567
|
if (ph instanceof Element) S.io?.unobserve(ph);
|
|
423
568
|
const id = parseInt(w.getAttribute(A_WRAPID), 10);
|
|
424
|
-
if (Number.isFinite(id)) S.mountedIds.delete(id);
|
|
569
|
+
if (Number.isFinite(id)) { S.ezActiveIds.delete(id); S.mountedIds.delete(id); }
|
|
425
570
|
const key = w.getAttribute(A_ANCHOR);
|
|
426
571
|
if (key && S.wrapByKey.get(key) === w) S.wrapByKey.delete(key);
|
|
427
572
|
w.remove();
|
|
@@ -496,7 +641,8 @@
|
|
|
496
641
|
const key = anchorKey(klass, el);
|
|
497
642
|
if (findWrap(key)) continue;
|
|
498
643
|
|
|
499
|
-
|
|
644
|
+
let id = pickId(poolKey);
|
|
645
|
+
if (!id) { sweepDeadWraps(); id = pickId(poolKey); }
|
|
500
646
|
if (id) {
|
|
501
647
|
const w = insertAfter(el, id, klass, key);
|
|
502
648
|
if (w) { observePh(id); inserted++; }
|
|
@@ -527,76 +673,107 @@
|
|
|
527
673
|
}
|
|
528
674
|
|
|
529
675
|
function observePh(id) {
|
|
530
|
-
const ph =
|
|
676
|
+
const ph = phEl(id);
|
|
531
677
|
if (ph?.isConnected) try { getIO()?.observe(ph); } catch (_) {}
|
|
678
|
+
// Fast-path : si déjà proche viewport, ne pas attendre un callback IO complet
|
|
679
|
+
try {
|
|
680
|
+
if (!ph?.isConnected) return;
|
|
681
|
+
const rect = ph.getBoundingClientRect();
|
|
682
|
+
const vh = window.innerHeight || 800;
|
|
683
|
+
const preload = isMobile() ? 1400 : 1000;
|
|
684
|
+
if (rect.top <= vh + preload && rect.bottom >= -preload) enqueueShow(id);
|
|
685
|
+
} catch (_) {}
|
|
532
686
|
}
|
|
533
687
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
688
|
+
function enqueueShow(id) {
|
|
689
|
+
if (!id || isBlocked()) return;
|
|
690
|
+
const n = parseInt(id, 10);
|
|
691
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
692
|
+
if (ts() - (S.lastShow.get(n) ?? 0) < SHOW_THROTTLE_MS) return;
|
|
693
|
+
if (!S.pendingSet.has(n)) { S.pending.push(n); S.pendingSet.add(n); }
|
|
694
|
+
scheduleDrainQueue();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function scheduleDrainQueue() {
|
|
698
|
+
if (isBlocked()) return;
|
|
699
|
+
if (S.showBatchTimer) return;
|
|
700
|
+
S.showBatchTimer = setTimeout(() => {
|
|
701
|
+
S.showBatchTimer = 0;
|
|
702
|
+
drainQueue();
|
|
703
|
+
}, BATCH_FLUSH_MS);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function drainQueue() {
|
|
707
|
+
if (isBlocked()) return;
|
|
708
|
+
const free = Math.max(0, MAX_INFLIGHT - S.inflight);
|
|
709
|
+
if (!free || !S.pending.length) return;
|
|
710
|
+
|
|
711
|
+
const picked = [];
|
|
712
|
+
const seen = new Set();
|
|
713
|
+
const batchCap = Math.max(1, Math.min(MAX_SHOW_BATCH, free, getDynamicShowBatchMax()));
|
|
714
|
+
while (S.pending.length && picked.length < batchCap) {
|
|
715
|
+
const id = S.pending.shift();
|
|
716
|
+
S.pendingSet.delete(id);
|
|
717
|
+
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
718
|
+
if (!phEl(id)?.isConnected) continue;
|
|
719
|
+
seen.add(id);
|
|
720
|
+
picked.push(id);
|
|
542
721
|
}
|
|
722
|
+
if (picked.length) startShowBatch(picked);
|
|
723
|
+
if (S.pending.length && (MAX_INFLIGHT - S.inflight) > 0) scheduleDrainQueue();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function startShowBatch(ids) {
|
|
727
|
+
if (!ids?.length || isBlocked()) return;
|
|
728
|
+
const reserve = ids.length;
|
|
729
|
+
S.inflight += reserve;
|
|
730
|
+
|
|
731
|
+
let done = false;
|
|
732
|
+
const release = () => {
|
|
733
|
+
if (done) return;
|
|
734
|
+
done = true;
|
|
735
|
+
S.inflight = Math.max(0, S.inflight - reserve);
|
|
736
|
+
drainQueue();
|
|
737
|
+
};
|
|
738
|
+
const timer = setTimeout(release, SHOW_FAILSAFE_MS);
|
|
543
739
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const id = S.pending.shift();
|
|
548
|
-
S.pendingSet.delete(id);
|
|
549
|
-
startShow(id);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
740
|
+
requestAnimationFrame(() => {
|
|
741
|
+
try {
|
|
742
|
+
if (isBlocked()) { clearTimeout(timer); return release(); }
|
|
552
743
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
S.inflight++;
|
|
556
|
-
let done = false;
|
|
557
|
-
const release = () => {
|
|
558
|
-
if (done) return;
|
|
559
|
-
done = true;
|
|
560
|
-
S.inflight = Math.max(0, S.inflight - 1);
|
|
561
|
-
drainQueue();
|
|
562
|
-
};
|
|
563
|
-
const timer = setTimeout(release, 7000);
|
|
744
|
+
const valid = [];
|
|
745
|
+
const t = ts();
|
|
564
746
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
const ph =
|
|
569
|
-
if (!
|
|
747
|
+
for (const raw of ids) {
|
|
748
|
+
const id = parseInt(raw, 10);
|
|
749
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
750
|
+
const ph = phEl(id);
|
|
751
|
+
if (!canShowPlaceholderId(id, t)) continue;
|
|
570
752
|
|
|
571
|
-
const t = ts();
|
|
572
|
-
if (t - (S.lastShow.get(id) ?? 0) < SHOW_THROTTLE_MS) { clearTimeout(timer); return release(); }
|
|
573
753
|
S.lastShow.set(id, t);
|
|
574
|
-
|
|
575
754
|
try { ph.closest?.(`.${WRAP_CLASS}`)?.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
755
|
+
valid.push(id);
|
|
756
|
+
}
|
|
576
757
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
758
|
+
if (!valid.length) { clearTimeout(timer); return release(); }
|
|
759
|
+
|
|
760
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
761
|
+
const ez = window.ezstandalone;
|
|
762
|
+
const doShow = () => {
|
|
763
|
+
const prepared = destroyBeforeReuse(valid);
|
|
764
|
+
if (!prepared.length) { setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS); return; }
|
|
765
|
+
try { ez.showAds(...prepared); } catch (_) {}
|
|
766
|
+
for (const id of prepared) {
|
|
767
|
+
S.ezActiveIds.add(id);
|
|
768
|
+
S.ezShownSinceDestroy.add(id);
|
|
769
|
+
}
|
|
770
|
+
setTimeout(() => { clearTimeout(timer); release(); }, SHOW_RELEASE_MS);
|
|
771
|
+
};
|
|
772
|
+
Array.isArray(ez.cmd) ? ez.cmd.push(doShow) : doShow();
|
|
773
|
+
} catch (_) { clearTimeout(timer); release(); }
|
|
774
|
+
});
|
|
775
|
+
}
|
|
588
776
|
|
|
589
|
-
function scheduleEmptyCheck(id, showTs) {
|
|
590
|
-
setTimeout(() => {
|
|
591
|
-
try {
|
|
592
|
-
const ph = document.getElementById(`${PH_PREFIX}${id}`);
|
|
593
|
-
const wrap = ph?.closest?.(`.${WRAP_CLASS}`);
|
|
594
|
-
if (!wrap || !ph?.isConnected) return;
|
|
595
|
-
if (parseInt(wrap.getAttribute(A_SHOWN) || '0', 10) > showTs) return;
|
|
596
|
-
wrap.classList.toggle('is-empty', !isFilled(ph));
|
|
597
|
-
} catch (_) {}
|
|
598
|
-
}, EMPTY_CHECK_MS);
|
|
599
|
-
}
|
|
600
777
|
|
|
601
778
|
// ── Patch Ezoic showAds ────────────────────────────────────────────────────
|
|
602
779
|
//
|
|
@@ -614,14 +791,21 @@
|
|
|
614
791
|
const orig = ez.showAds.bind(ez);
|
|
615
792
|
ez.showAds = function (...args) {
|
|
616
793
|
if (isBlocked()) return;
|
|
617
|
-
const ids
|
|
794
|
+
const ids = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
795
|
+
const valid = [];
|
|
618
796
|
const seen = new Set();
|
|
619
797
|
for (const v of ids) {
|
|
620
798
|
const id = parseInt(v, 10);
|
|
621
799
|
if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
|
|
622
|
-
if (!
|
|
800
|
+
if (!phEl(id)?.isConnected || !hasSinglePlaceholder(id)) continue;
|
|
623
801
|
seen.add(id);
|
|
624
|
-
|
|
802
|
+
valid.push(id);
|
|
803
|
+
}
|
|
804
|
+
if (!valid.length) return;
|
|
805
|
+
try { orig(...valid); } catch (_) {
|
|
806
|
+
for (const id of valid) {
|
|
807
|
+
try { orig(id); } catch (_) {}
|
|
808
|
+
}
|
|
625
809
|
}
|
|
626
810
|
};
|
|
627
811
|
} catch (_) {}
|
|
@@ -638,6 +822,7 @@
|
|
|
638
822
|
async function runCore() {
|
|
639
823
|
if (isBlocked()) return 0;
|
|
640
824
|
patchShowAds();
|
|
825
|
+
sweepDeadWraps();
|
|
641
826
|
|
|
642
827
|
const cfg = await fetchConfig();
|
|
643
828
|
if (!cfg || cfg.excluded) return 0;
|
|
@@ -704,7 +889,7 @@
|
|
|
704
889
|
S.burstCount++;
|
|
705
890
|
scheduleRun(n => {
|
|
706
891
|
if (!n && !S.pending.length) { S.burstActive = false; return; }
|
|
707
|
-
setTimeout(step, n > 0 ?
|
|
892
|
+
setTimeout(step, n > 0 ? 80 : 180);
|
|
708
893
|
});
|
|
709
894
|
};
|
|
710
895
|
step();
|
|
@@ -722,11 +907,21 @@
|
|
|
722
907
|
S.mountedIds.clear();
|
|
723
908
|
S.lastShow.clear();
|
|
724
909
|
S.wrapByKey.clear();
|
|
910
|
+
S.ezActiveIds.clear();
|
|
911
|
+
S.ezShownSinceDestroy.clear();
|
|
725
912
|
S.inflight = 0;
|
|
726
913
|
S.pending = [];
|
|
727
914
|
S.pendingSet.clear();
|
|
915
|
+
if (S.showBatchTimer) { clearTimeout(S.showBatchTimer); S.showBatchTimer = 0; }
|
|
916
|
+
if (S.destroyBatchTimer) { clearTimeout(S.destroyBatchTimer); S.destroyBatchTimer = 0; }
|
|
917
|
+
S.destroyPending = [];
|
|
918
|
+
S.destroyPendingSet.clear();
|
|
728
919
|
S.burstActive = false;
|
|
729
920
|
S.runQueued = false;
|
|
921
|
+
S.sweepQueued = false;
|
|
922
|
+
S.scrollSpeed = 0;
|
|
923
|
+
S.lastScrollY = 0;
|
|
924
|
+
S.lastScrollTs = 0;
|
|
730
925
|
}
|
|
731
926
|
|
|
732
927
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
@@ -737,8 +932,17 @@
|
|
|
737
932
|
S.domObs = new MutationObserver(muts => {
|
|
738
933
|
if (S.mutGuard > 0 || isBlocked()) return;
|
|
739
934
|
for (const m of muts) {
|
|
935
|
+
let sawWrapRemoval = false;
|
|
936
|
+
for (const n of m.removedNodes) {
|
|
937
|
+
if (n.nodeType !== 1) continue;
|
|
938
|
+
if ((n.matches && n.matches(`.${WRAP_CLASS}`)) || (n.querySelector && n.querySelector(`.${WRAP_CLASS}`))) {
|
|
939
|
+
sawWrapRemoval = true;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (sawWrapRemoval) queueSweepDeadWraps();
|
|
740
943
|
for (const n of m.addedNodes) {
|
|
741
944
|
if (n.nodeType !== 1) continue;
|
|
945
|
+
try { healFalseEmpty(n); } catch (_) {}
|
|
742
946
|
// matches() d'abord (O(1)), querySelector() seulement si nécessaire
|
|
743
947
|
if (allSel.some(s => { try { return n.matches(s); } catch(_){return false;} }) ||
|
|
744
948
|
allSel.some(s => { try { return !!n.querySelector(s); } catch(_){return false;} })) {
|
|
@@ -824,7 +1028,7 @@
|
|
|
824
1028
|
S.pageKey = pageKey();
|
|
825
1029
|
blockedUntil = 0;
|
|
826
1030
|
muteConsole(); ensureTcfLocator(); warmNetwork();
|
|
827
|
-
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
1031
|
+
patchShowAds(); getIO(); ensureDomObserver(); sweepDeadWraps(); requestBurst();
|
|
828
1032
|
});
|
|
829
1033
|
|
|
830
1034
|
const burstEvts = [
|
|
@@ -846,12 +1050,21 @@
|
|
|
846
1050
|
|
|
847
1051
|
function bindScroll() {
|
|
848
1052
|
let ticking = false;
|
|
1053
|
+
try {
|
|
1054
|
+
S.lastScrollY = window.scrollY || window.pageYOffset || 0;
|
|
1055
|
+
S.lastScrollTs = ts();
|
|
1056
|
+
} catch (_) {}
|
|
849
1057
|
window.addEventListener('scroll', () => {
|
|
850
1058
|
try {
|
|
851
1059
|
const y = window.scrollY || window.pageYOffset || 0;
|
|
1060
|
+
const t = ts();
|
|
852
1061
|
const dy = y - (S.lastScrollY || 0);
|
|
853
|
-
|
|
1062
|
+
const dt = Math.max(1, t - (S.lastScrollTs || t));
|
|
1063
|
+
if (Math.abs(dy) > 1) S.scrollDir = dy >= 0 ? 1 : -1;
|
|
1064
|
+
const inst = Math.abs(dy) * 1000 / dt;
|
|
1065
|
+
S.scrollSpeed = S.scrollSpeed ? (S.scrollSpeed * 0.7 + inst * 0.3) : inst;
|
|
854
1066
|
S.lastScrollY = y;
|
|
1067
|
+
S.lastScrollTs = t;
|
|
855
1068
|
} catch (_) {}
|
|
856
1069
|
if (ticking) return;
|
|
857
1070
|
ticking = true;
|
|
@@ -862,7 +1075,6 @@
|
|
|
862
1075
|
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
863
1076
|
|
|
864
1077
|
S.pageKey = pageKey();
|
|
865
|
-
try { S.lastScrollY = window.scrollY || window.pageYOffset || 0; } catch (_) { S.lastScrollY = 0; }
|
|
866
1078
|
muteConsole();
|
|
867
1079
|
ensureTcfLocator();
|
|
868
1080
|
warmNetwork();
|
package/public/style.css
CHANGED
|
@@ -56,23 +56,17 @@
|
|
|
56
56
|
top: auto !important;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/* ── État vide ────────────────────────────────────────────────────────────── */
|
|
60
|
-
/*
|
|
61
|
-
Ajouté 20s après showAds si aucun fill détecté.
|
|
62
|
-
Collapse à 1px (pas 0) : reste observable par l'IO si le fill arrive tard.
|
|
63
|
-
*/
|
|
64
|
-
.nodebb-ezoic-wrap.is-empty {
|
|
65
|
-
display: block !important;
|
|
66
|
-
height: 1px !important;
|
|
67
|
-
min-height: 1px !important;
|
|
68
|
-
max-height: 1px !important;
|
|
69
|
-
margin: 0 !important;
|
|
70
|
-
padding: 0 !important;
|
|
71
|
-
overflow: hidden !important;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
59
|
/* ── Ezoic global (hors de nos wraps) ────────────────────────────────────── */
|
|
75
60
|
.ezoic-ad {
|
|
76
61
|
margin: 0 !important;
|
|
77
62
|
padding: 0 !important;
|
|
78
63
|
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/* Filet anti faux-empty : si la pub est rendue, ne pas laisser le wrap replié */
|
|
67
|
+
.nodebb-ezoic-wrap.is-empty:has(iframe, [data-google-container-id], [id^="google_ads_iframe_"]) {
|
|
68
|
+
height: auto !important;
|
|
69
|
+
min-height: 1px !important;
|
|
70
|
+
max-height: none !important;
|
|
71
|
+
overflow: visible !important;
|
|
72
|
+
}
|