@@ -31,6 +31,7 @@ function queueAfterPaintCallback(callback: VoidFunction) {
3131/** Runs all callbacks that still need to complete before the page is hidden/unloaded. */
3232function resolvePendingPromises ( ) {
3333 while ( pendingCallbacks . size ) {
34+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3435 const callback = pendingCallbacks . values ( ) . next ( ) . value !
3536 pendingCallbacks . delete ( callback )
3637 callback ( )
@@ -44,8 +45,8 @@ declare const scheduler: {
4445const lowPriorityCallback =
4546 "scheduler" in window && "postTask" in scheduler
4647 ? ( cb : VoidFunction ) => {
47- scheduler . postTask ( cb , { priority : "background" } )
48- }
48+ scheduler . postTask ( cb , { priority : "background" } )
49+ }
4950 : ( cb : VoidFunction ) => setTimeout ( cb , 1 )
5051
5152let loadPromise : Promise < void > | undefined = new Promise < void > ( resolve => {
@@ -82,9 +83,9 @@ async function queueYieldCallback(callback: VoidFunction, shouldWaitForLoad: boo
8283 }
8384 }
8485
85- // Callback has already been run
86+ // Callback has already been run
8687 if ( ! pendingCallbacks . has ( callback ) ) return
87-
88+
8889 if ( document . hidden ) {
8990 // The tab may have been hidden while we were waiting for load; don't leave this callback behind
9091 // for a rAF that may never run.
@@ -175,15 +176,15 @@ document.addEventListener(
175176 globalClickReceivedListener ( )
176177 }
177178 } ,
178- true ,
179+ true
179180)
180181document . addEventListener (
181182 "pagehide" ,
182183 ( ) => {
183184 globalClickReceivedListener ( )
184185 resolvePendingPromises ( )
185186 } ,
186- true ,
187+ true
187188)
188189
189190type DataLayerPush = ( ...items : object [ ] ) => boolean
@@ -220,7 +221,7 @@ function logEventDiff(event: Event, newEvent: Event) {
220221document . addEventListener = function (
221222 type : string ,
222223 listener : EventListenerOrEventListenerObject ,
223- options : boolean | AddEventListenerOptions | undefined ,
224+ options : boolean | AddEventListenerOptions | undefined
224225) {
225226 if ( typesToIntercept . includes ( type as EventType ) ) {
226227 if ( DEBUG ) console . log ( `Overriding ${ type } listener` , listener )
@@ -256,7 +257,7 @@ document.addEventListener = function (
256257 }
257258 } )
258259 } ,
259- options ,
260+ options
260261 )
261262 return
262263 }
@@ -348,28 +349,51 @@ const gtmObserver = new MutationObserver(() => {
348349gtmObserver . observe ( document . documentElement , { childList : true , subtree : true } )
349350
350351// #region History/submit wrapper override
351- const originalMethodsCalledInCurrentChain = new Set < Function > ( )
352-
353- function callOriginalMethod ( this : unknown , originalMethod : Function , args : unknown [ ] ) {
354- if ( originalMethodsCalledInCurrentChain . has ( originalMethod ) ) return
355- originalMethod . apply ( this , args )
356- }
352+ /**
353+ * History/form overrides usually chain by capturing the previous function:
354+ *
355+ * ```js
356+ * const previousPushState = history.pushState
357+ * history.pushState = function (...args) {
358+ * previousPushState.apply(this, args)
359+ * // 3p side effects
360+ * }
361+ * ```
362+ *
363+ * Our wrapper calls the native method immediately, then yields before running the 3p override body. If the
364+ * override body calls a captured older wrapper, that older wrapper must not call the native method again for
365+ * the same top-level navigation/submit. Each wrapper therefore gets a generation number, and while wrapper N
366+ * runs its override body we mark generations `< N` as "native already handled".
367+ *
368+ * Fresh nested calls still work: if an override intentionally calls `history.pushState(...)` again, it goes
369+ * through the current wrapper, whose generation is `>= N`, so it still calls native. This preserves real nested
370+ * navigations while suppressing duplicate native calls from captured older wrappers, including stale captures
371+ * that are older than the immediately previous wrapper.
372+ */
373+ const skippedWrappedListenerGenerations = new Map < Function , number > ( )
374+ let nextWrappedListenerGeneration = 0
357375
358- function withOriginalMethodAlreadyCalled ( originalMethod : Function , callback : VoidFunction ) {
359- const alreadyMarked = originalMethodsCalledInCurrentChain . has ( originalMethod )
360- originalMethodsCalledInCurrentChain . add ( originalMethod )
376+ function withOlderWrappedListenersSkipped ( originalMethod : Function , generation : number , callback : VoidFunction ) {
377+ const previousSkippedGeneration = skippedWrappedListenerGenerations . get ( originalMethod )
378+ skippedWrappedListenerGenerations . set (
379+ originalMethod ,
380+ previousSkippedGeneration === undefined ? generation : Math . max ( previousSkippedGeneration , generation )
381+ )
361382 try {
362383 callback ( )
363384 } finally {
364- if ( ! alreadyMarked ) {
365- originalMethodsCalledInCurrentChain . delete ( originalMethod )
385+ if ( previousSkippedGeneration === undefined ) {
386+ skippedWrappedListenerGenerations . delete ( originalMethod )
387+ } else {
388+ skippedWrappedListenerGenerations . set ( originalMethod , previousSkippedGeneration )
366389 }
367390 }
368391}
369392
370- function wrapListener ( originalMethod : Function , value : Function ) {
393+ function wrapListener ( originalMethod : Function , value ?: Function ) {
394+ const generation = nextWrappedListenerGeneration ++
371395 // the function syntax is important here so we keep the correct `this`.
372- return function yieldingListener ( this : unknown , ...args : unknown [ ] ) {
396+ function yieldingListener ( this : unknown , ...args : unknown [ ] ) {
373397 if ( DEBUG ) {
374398 console . log ( "Yielding for" , originalMethod )
375399 console . timeStamp ( originalMethod as unknown as string )
@@ -378,29 +402,31 @@ function wrapListener(originalMethod: Function, value: Function) {
378402 // We first call the original: This optimizes for UX & correctness of React components.
379403 // e.g., for pushState, when a component renders on a new route, it might set state and/or read from the URL. If the URL isn't
380404 // accurate, it might lead to wrong behavior.
381- // We don't want to call the underlying native method twice if an override calls a previously captured wrapper.
382- callOriginalMethod . call ( this , originalMethod , args )
405+ // If an override calls a previously captured wrapper, that older wrapper skips the native method;
406+ // fresh calls through the current getter still update history/submit normally.
407+ const skippedGeneration = skippedWrappedListenerGenerations . get ( originalMethod )
408+ if ( skippedGeneration === undefined || generation >= skippedGeneration ) {
409+ originalMethod . apply ( this , args )
410+ }
411+
412+ if ( ! value ) return
383413
384414 // If `method` is overriden N times, it creates N yield points (as overrides might be chained)
385415 yieldUnlessUrgent ( ( ) => {
386416 // The arrow FN is important here so we keep the correct `this`.
387- withOriginalMethodAlreadyCalled ( originalMethod , ( ) => {
417+ withOlderWrappedListenersSkipped ( originalMethod , generation , ( ) => {
388418 value . apply ( this , args )
389419 } )
390420 } )
391421 }
422+
423+ return yieldingListener
392424}
393425function overrideListener < T extends object > ( target : T , method : keyof T ) {
394426 // @ts -expect-error TS(2339): Prototype chain call. We try __proto__ first, as this will usually be the original method.
395427 const originalMethod : Function = ( target . __proto__ as unknown as T ) [ method ] ?? ( target [ method ] as Function )
396428
397- let mostRecentWrapper : Function | undefined = wrapListener (
398- originalMethod ,
399- // The function syntax is important here so we keep the correct `this`.
400- function firstOverride ( this : unknown , ...args : unknown [ ] ) {
401- callOriginalMethod . call ( this , originalMethod , args )
402- } ,
403- )
429+ let mostRecentWrapper : Function | undefined = wrapListener ( originalMethod )
404430
405431 Object . defineProperty ( target , method , {
406432 enumerable : true ,
0 commit comments