nodebb-plugin-ezoic-infinite 1.8.41 → 1.8.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -144,8 +144,7 @@ const EZOIC_SCRIPTS = [
144
144
  '<script async src="//www.ezojs.com/ezoic/sa.min.js"></script>',
145
145
  '<script>',
146
146
  'window.ezstandalone = window.ezstandalone || {};',
147
- // Always reference through window to avoid ReferenceError in stricter contexts
148
- 'window.ezstandalone.cmd = window.ezstandalone.cmd || [];',
147
+ 'ezstandalone.cmd = ezstandalone.cmd || [];',
149
148
  '</script>',
150
149
  ].join('\n');
151
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.8.41",
3
+ "version": "1.8.42",
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
@@ -38,7 +38,7 @@
38
38
  SHOW_RELEASE_MS: 700,
39
39
  BATCH_FLUSH_MS: 80,
40
40
  RECYCLE_DELAY_MS: 450,
41
-
41
+ TCF_DEBOUNCE_MS: 500,
42
42
  };
43
43
 
44
44
  // Limits
@@ -257,54 +257,6 @@
257
257
  return set;
258
258
  }
259
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 (_) {}
306
- }
307
-
308
260
  // ── Wrap lifecycle detection ───────────────────────────────────────────────
309
261
 
310
262
  /**
@@ -813,10 +765,6 @@
813
765
  async function runCore() {
814
766
  if (isBlocked()) return 0;
815
767
 
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
-
820
768
  const cfg = await fetchConfig();
821
769
  if (!cfg || cfg.excluded) return 0;
822
770
  initPools(cfg);
@@ -940,22 +888,6 @@
940
888
 
941
889
  for (const m of muts) {
942
890
  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
-
959
891
  for (const node of m.addedNodes) {
960
892
  if (!(node instanceof Element)) continue;
961
893
 
@@ -991,122 +923,50 @@
991
923
  } catch (_) {}
992
924
  }
993
925
 
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):
926
+ // ── TCF Locator ────────────────────────────────────────────────────────────
1007
927
  //
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>.
928
+ // FIX: The CMP iframe locator (__tcfapiLocator) must exist for the CMP to
929
+ // communicate via postMessage. NodeBB's ajaxify can remove it during navigation.
1011
930
  //
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.
931
+ // Previous approach: MutationObserver that re-injects on EVERY mutation race
932
+ // conditions with CMP, causing "Cannot read properties of null (reading 'postMessage')"
933
+ // and "Cannot set properties of null (setting 'addtlConsent')".
1015
934
  //
1016
- // 3. RESTORE: MutationObserver on <body> childList (not subtree) to
1017
- // immediately re-create the locator if something still removes it.
935
+ // New approach: Debounced check only re-inject after DOM stabilizes (500ms).
936
+ // The CMP scripts handle their own initialization; we just ensure the locator
937
+ // iframe exists when needed.
938
+
939
+ let _tcfDebounce = null;
1018
940
 
1019
941
  function ensureTcfLocator() {
942
+ // Only needed if CMP APIs are present
1020
943
  if (!window.__tcfapi && !window.__cmp) return;
1021
944
 
1022
- const LOCATOR_ID = '__tcfapiLocator';
1023
-
1024
- // Create or relocate the locator iframe into <head> for protection
1025
- const ensureInHead = () => {
1026
- let existing = document.getElementById(LOCATOR_ID);
1027
- if (existing) {
1028
- // If it's in <body>, move it to <head> where ajaxify can't reach it
1029
- if (existing.parentElement !== document.head) {
1030
- try { document.head.appendChild(existing); } catch (_) {}
1031
- }
1032
- return existing;
1033
- }
1034
- // Create fresh
945
+ const inject = () => {
946
+ if (document.getElementById('__tcfapiLocator')) return;
1035
947
  const f = document.createElement('iframe');
1036
948
  f.style.display = 'none';
1037
- f.id = f.name = LOCATOR_ID;
1038
- try { document.head.appendChild(f); } catch (_) {
1039
- // Fallback to body if head insertion fails
1040
- (document.body || document.documentElement).appendChild(f);
1041
- }
1042
- return f;
949
+ f.id = f.name = '__tcfapiLocator';
950
+ (document.body || document.documentElement).appendChild(f);
1043
951
  };
1044
952
 
1045
- ensureInHead();
1046
-
1047
- // Layer 2: Guard the CMP API calls against null contentWindow
1048
- if (!window.__nbbCmpGuarded) {
1049
- window.__nbbCmpGuarded = true;
1050
-
1051
- // Wrap __tcfapi
1052
- if (typeof window.__tcfapi === 'function') {
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
- }
1067
- };
1068
- }
953
+ inject();
1069
954
 
1070
- // Wrap __cmp (legacy CMP v1 API)
1071
- if (typeof window.__cmp === 'function') {
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
- }
1082
- };
1083
- }
1084
- }
1085
-
1086
- // Layer 3: MutationObserver to immediately restore if removed
955
+ // Set up a debounced observer instead of per-mutation injection
1087
956
  if (!window.__nbbTcfObs) {
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();
957
+ window.__nbbTcfObs = new MutationObserver(() => {
958
+ // Quick check: if locator still exists, skip
959
+ if (document.getElementById('__tcfapiLocator')) return;
960
+ // Debounce: wait for DOM to stabilize before re-injecting
961
+ clearTimeout(_tcfDebounce);
962
+ _tcfDebounce = setTimeout(inject, TIMING.TCF_DEBOUNCE_MS);
963
+ });
964
+ // Observe only direct children of body (not deep subtree)
965
+ // since the locator iframe is a direct child
966
+ window.__nbbTcfObs.observe(document.body || document.documentElement, {
967
+ childList: true,
968
+ subtree: false,
1093
969
  });
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 (_) {}
1110
970
  }
1111
971
  }
1112
972
 
@@ -1125,8 +985,6 @@
1125
985
  'cannot call refresh on the same page',
1126
986
  'no placeholders are currently defined in Refresh',
1127
987
  'Debugger iframe already exists',
1128
- '[CMP] Error in custom getTCData',
1129
- 'vignette: no interstitial API',
1130
988
  ];
1131
989
  const PH_PATTERN = `with id ${PH_PREFIX}`;
1132
990
 
@@ -1192,9 +1050,6 @@
1192
1050
  state.kind = null;
1193
1051
  state.blockedUntil = 0;
1194
1052
 
1195
- // Fix: CMP modal can leave aria-hidden="true" on <body> after navigation
1196
- try { document.body.removeAttribute('aria-hidden'); } catch (_) {}
1197
-
1198
1053
  muteConsole();
1199
1054
  ensureTcfLocator();
1200
1055
  warmNetwork();