nodebb-plugin-ezoic-infinite 1.7.2 → 1.7.3

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 +71 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -261,17 +261,45 @@
261
261
  return w;
262
262
  }
263
263
 
264
+ /**
265
+ * Retire proprement un wrap du DOM.
266
+ *
267
+ * Sans précaution, retirer un wrap contenant un player vidéo Ezoic (wyvern.js)
268
+ * déclenche des erreurs async sur des nœuds détachés :
269
+ * "Cannot read properties of null (reading 'paused')"
270
+ * "Cannot read properties of null (reading 'offsetWidth')"
271
+ * "Invalid target for null#trigger / null#on"
272
+ *
273
+ * On pause les media et on tente de notifier l'API wyvern avant remove().
274
+ */
264
275
  function dropWrap(w) {
265
276
  try {
266
- const id = parseInt(w.getAttribute(A_WRAPID), 10);
267
- if (Number.isFinite(id)) S.mountedIds.delete(id);
268
- // IMPORTANT : ne passer unobserve que si c'est un vrai Element.
269
- // unobserve(null) corrompt l'état interne de l'IO (pubads lève ensuite
270
- // "parameter 1 is not of type Element" sur le prochain observe).
277
+ // 1. Pause tous les media actifs avant détachement
278
+ try {
279
+ w.querySelectorAll('video, audio').forEach(m => {
280
+ try { if (!m.paused) m.pause(); } catch (_) {}
281
+ });
282
+ } catch (_) {}
283
+
284
+ // 2. Notifier l'API wyvern si disponible
285
+ try {
286
+ if (window.wyvern && typeof window.wyvern === 'object') {
287
+ w.querySelectorAll('[id^="ezoic-"],[class*="wyvern"],[class*="ezoic-video"]')
288
+ .forEach(n => { try { window.wyvern.destroy?.(n); } catch (_) {} });
289
+ }
290
+ } catch (_) {}
291
+
292
+ // 3. Unobserve (guard instanceof Element — unobserve(null) corrompt l'IO)
271
293
  try {
272
294
  const ph = w.querySelector(`[id^="${PH_PREFIX}"]`);
273
295
  if (ph instanceof Element) S.io?.unobserve(ph);
274
296
  } catch (_) {}
297
+
298
+ // 4. Libérer l'id du pool
299
+ const id = parseInt(w.getAttribute(A_WRAPID), 10);
300
+ if (Number.isFinite(id)) S.mountedIds.delete(id);
301
+
302
+ // 5. Retrait DOM
275
303
  w.remove();
276
304
  } catch (_) {}
277
305
  }
@@ -289,17 +317,31 @@
289
317
  *
290
318
  * On ne prune pas les wraps < MIN_PRUNE_AGE_MS (DOM pas encore stabilisé).
291
319
  */
320
+ /**
321
+ * Supprime les wraps dont l'élément-ancre n'est plus dans le DOM.
322
+ *
323
+ * Règle stricte : on ne supprime JAMAIS un wrap rempli (filled).
324
+ * - Il peut contenir un player wyvern actif → .remove() déclenche des
325
+ * erreurs async ("Cannot read 'paused'", "offsetWidth", "getChild"…).
326
+ * - Le post-ancre peut être temporairement virtualisé par NodeBB puis
327
+ * revenir — dans ce cas le wrap filled doit rester en place.
328
+ *
329
+ * Seuls les wraps VIDES dont l'ancre a disparu sont supprimés.
330
+ */
292
331
  function pruneOrphans(klass) {
293
332
  const meta = KIND[klass];
294
333
  if (!meta) return;
295
334
 
296
- const baseTag = meta.sel.split('[')[0]; // ex: "li", "[component=..." → "" (géré)
335
+ const baseTag = meta.sel.split('[')[0];
297
336
 
298
337
  document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
299
338
  if (ts() - parseInt(w.getAttribute(A_CREATED) || '0', 10) < MIN_PRUNE_AGE_MS) return;
300
339
 
340
+ // Ne jamais retirer un wrap qui contient du contenu (player potentiellement actif)
341
+ if (isFilled(w)) return;
342
+
301
343
  const key = w.getAttribute(A_ANCHOR) ?? '';
302
- const sid = key.slice(klass.length + 1); // extrait la partie après "kindClass:"
344
+ const sid = key.slice(klass.length + 1);
303
345
  if (!sid) { mutate(() => dropWrap(w)); return; }
304
346
 
305
347
  const anchorEl = document.querySelector(
@@ -314,11 +356,12 @@
314
356
  /**
315
357
  * Deux wraps adjacents = situation anormale → supprimer le moins prioritaire.
316
358
  * Priorité : filled > en grâce (fill en cours) > vide.
317
- * Ne supprime jamais un wrap dont showAds() date de moins de FILL_GRACE_MS.
359
+ *
360
+ * Règle de sécurité wyvern : on ne supprime JAMAIS un wrap rempli.
361
+ * Si les deux wraps adjacents sont remplis, on laisse en place (edge case rare).
318
362
  */
319
363
  function decluster(klass) {
320
364
  for (const w of document.querySelectorAll(`.${WRAP_CLASS}.${klass}`)) {
321
- // Grace sur le wrap courant : on le saute entièrement
322
365
  const wShown = parseInt(w.getAttribute(A_SHOWN) || '0', 10);
323
366
  if (wShown && ts() - wShown < FILL_GRACE_MS) continue;
324
367
 
@@ -327,10 +370,16 @@
327
370
  if (!prev.classList?.contains(WRAP_CLASS)) { prev = prev.previousElementSibling; continue; }
328
371
 
329
372
  const pShown = parseInt(prev.getAttribute(A_SHOWN) || '0', 10);
330
- if (pShown && ts() - pShown < FILL_GRACE_MS) break; // précédent en grâce → rien
373
+ if (pShown && ts() - pShown < FILL_GRACE_MS) break;
374
+
375
+ const wFilled = isFilled(w);
376
+ const pFilled = isFilled(prev);
331
377
 
332
- if (!isFilled(w)) mutate(() => dropWrap(w));
333
- else if (!isFilled(prev)) mutate(() => dropWrap(prev));
378
+ // Ne jamais retirer un wrap rempli (player actif potentiel)
379
+ if (!wFilled && !pFilled) mutate(() => dropWrap(w)); // les deux vides → retirer le courant
380
+ else if (!wFilled && pFilled) mutate(() => dropWrap(w)); // courant vide, précédent rempli → retirer le courant
381
+ else if ( wFilled && !pFilled) mutate(() => dropWrap(prev)); // courant rempli, précédent vide → retirer le précédent
382
+ // les deux remplis → rien (on ne touche pas)
334
383
  break;
335
384
  }
336
385
  }
@@ -592,6 +641,16 @@
592
641
 
593
642
  function cleanup() {
594
643
  blockedUntil = ts() + 1500;
644
+
645
+ // Pause tous les media dans nos wraps AVANT de les retirer du DOM.
646
+ // Empêche wyvern.js de continuer d'exécuter ses callbacks async sur des
647
+ // nœuds détachés (erreurs "Cannot read 'paused'", "offsetWidth"…).
648
+ try {
649
+ document.querySelectorAll(`.${WRAP_CLASS} video, .${WRAP_CLASS} audio`).forEach(m => {
650
+ try { if (!m.paused) m.pause(); } catch (_) {}
651
+ });
652
+ } catch (_) {}
653
+
595
654
  mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
596
655
  S.cfg = null;
597
656
  S.pools = { topics: [], posts: [], categories: [] };