@@ -76,6 +76,7 @@ export function createSharedTrackingClient(options = {}) {
7676 let state = { ...EMPTY_STATE } ;
7777 let frame ;
7878 let frameReady ;
79+ let frameReadyCancel ;
7980 let closed = false ;
8081 let suspended = false ;
8182 let localRefusal = false ;
@@ -104,11 +105,33 @@ export function createSharedTrackingClient(options = {}) {
104105 for ( const subscriber of failureSubscribers ) subscriber ( error ) ;
105106 return error ;
106107 } ;
108+ const recoverableFailures = new WeakSet ( ) ;
109+ const recoverableFailure = ( ) => {
110+ const error = trackingError ( ) ;
111+ recoverableFailures . add ( error ) ;
112+ return error ;
113+ } ;
107114 const discardFrame = ( ) => {
115+ if ( frameReadyCancel ) {
116+ const cancel = frameReadyCancel ;
117+ frameReadyCancel = undefined ;
118+ cancel ( ) ;
119+ return ;
120+ }
108121 frame ?. remove ( ) ;
109122 frame = undefined ;
110123 frameReady = undefined ;
111124 } ;
125+ const dropFailedFrame = failedFrame => {
126+ if ( ! failedFrame || frame !== failedFrame ) return ;
127+ discardFrame ( ) ;
128+ for ( const [ id , item ] of [ ...pending ] ) {
129+ if ( item . frame !== failedFrame ) continue ;
130+ pending . delete ( id ) ;
131+ clearTimeout ( item . timer ) ;
132+ item . reject ( trackingError ( ) ) ;
133+ }
134+ } ;
112135 const replaceNativeRealm = next => {
113136 discardFrame ( ) ;
114137 for ( const [ id , item ] of [ ...pending ] ) {
@@ -167,10 +190,16 @@ export function createSharedTrackingClient(options = {}) {
167190 pending . delete ( id ) ;
168191 clearTimeout ( item . timer ) ;
169192 const response = parseTrackingResponse ( event . data ) ;
170- if ( ! response || ! response . ok || ( response . deviceId && item . type !== 'associate' ) ) {
193+ if ( ! response || ( response . deviceId && item . type !== 'associate' ) ) {
171194 item . reject ( fail ( ) ) ;
172195 return ;
173196 }
197+ if ( ! response . ok ) {
198+ item . reject ( item . recoverUnavailable && response . error === 'unavailable'
199+ ? recoverableFailure ( )
200+ : fail ( ) ) ;
201+ return ;
202+ }
174203 const current = update ( response . state ) ;
175204 if ( item . type === 'associate' ) {
176205 if ( ! current || effectiveState ( ) . choice !== 'accepted' ||
@@ -188,42 +217,113 @@ export function createSharedTrackingClient(options = {}) {
188217 const ensureFrame = ( ) => {
189218 if ( closed || ! currentWindow || ! currentDocument ) return Promise . reject ( trackingError ( ) ) ;
190219 if ( frameReady ) return frameReady ;
191- frameReady = new Promise ( ( resolve , reject ) => {
192- frame = currentDocument . createElement ( 'iframe' ) ;
193- frame . src = bridge . href ;
194- frame . hidden = true ;
195- frame . title = 'Shared tracking preferences' ;
196- frame . referrerPolicy = 'no-referrer' ;
197- frame . setAttribute ( 'aria-hidden' , 'true' ) ;
198- frame . setAttribute ( 'tabindex' , '-1' ) ;
199- const timer = setTimeout ( ( ) => reject ( trackingError ( ) ) , timeoutMs ) ;
200- frame . addEventListener ( 'load' , ( ) => { clearTimeout ( timer ) ; resolve ( frame ) ; } , { once : true } ) ;
201- frame . addEventListener ( 'error' , ( ) => { clearTimeout ( timer ) ; reject ( trackingError ( ) ) ; } , { once : true } ) ;
202- currentDocument . body . append ( frame ) ;
220+ const candidate = currentDocument . createElement ( 'iframe' ) ;
221+ candidate . src = bridge . href ;
222+ candidate . hidden = true ;
223+ candidate . title = 'Shared tracking preferences' ;
224+ candidate . referrerPolicy = 'no-referrer' ;
225+ candidate . setAttribute ( 'aria-hidden' , 'true' ) ;
226+ candidate . setAttribute ( 'tabindex' , '-1' ) ;
227+ let settled = false ;
228+ let resolveReady ;
229+ let rejectReady ;
230+ const ready = new Promise ( ( resolve , reject ) => {
231+ resolveReady = resolve ;
232+ rejectReady = reject ;
203233 } ) ;
204- return frameReady ;
205- } ;
206- const request = async ( type , payload = { } , requestOptions = { } ) => {
207- let readyFrame ;
234+ frame = candidate ;
235+ frameReady = ready ;
236+ const reset = ( ) => {
237+ if ( settled ) return ;
238+ settled = true ;
239+ clearTimeout ( timer ) ;
240+ if ( frame === candidate ) frame = undefined ;
241+ if ( frameReady === ready ) frameReady = undefined ;
242+ if ( frameReadyCancel === reset ) frameReadyCancel = undefined ;
243+ candidate . remove ( ) ;
244+ rejectReady ( trackingError ( ) ) ;
245+ } ;
246+ const timer = setTimeout ( reset , timeoutMs ) ;
247+ frameReadyCancel = reset ;
248+ candidate . addEventListener ( 'load' , ( ) => {
249+ if ( settled ) return ;
250+ if ( closed || frame !== candidate || frameReady !== ready ) {
251+ reset ( ) ;
252+ return ;
253+ }
254+ settled = true ;
255+ clearTimeout ( timer ) ;
256+ if ( frameReadyCancel === reset ) frameReadyCancel = undefined ;
257+ resolveReady ( candidate ) ;
258+ } , { once : true } ) ;
259+ candidate . addEventListener ( 'error' , reset , { once : true } ) ;
208260 try {
209- readyFrame = await ensureFrame ( ) ;
261+ currentDocument . body . append ( candidate ) ;
210262 } catch {
211- throw fail ( ) ;
263+ reset ( ) ;
212264 }
213- if ( closed || readyFrame !== frame ) throw trackingError ( ) ;
265+ return ready ;
266+ } ;
267+ const request = ( type , payload = { } , requestOptions = { } ) => {
214268 const message = createTrackingRequest ( type , payload , ( ) => currentWindow . crypto . randomUUID ( ) ) ;
215269 return new Promise ( ( resolve , reject ) => {
270+ let settled = false ;
271+ let requestFrame ;
272+ const resolveOnce = value => {
273+ if ( settled ) return ;
274+ settled = true ;
275+ clearTimeout ( timer ) ;
276+ resolve ( value ) ;
277+ } ;
278+ const rejectOnce = error => {
279+ if ( settled ) return ;
280+ settled = true ;
281+ clearTimeout ( timer ) ;
282+ reject ( error ) ;
283+ } ;
216284 const timer = setTimeout ( ( ) => {
217285 pending . delete ( message . id ) ;
218- reject ( fail ( ) ) ;
286+ dropFailedFrame ( requestFrame ) ;
287+ rejectOnce ( fail ( ) ) ;
219288 } , requestOptions . timeoutMs ?? timeoutMs ) ;
220- pending . set ( message . id , { resolve, reject, timer, type, ...payload } ) ;
221- readyFrame . contentWindow . postMessage ( message , bridge . origin ) ;
289+ let readiness ;
290+ try {
291+ readiness = ensureFrame ( ) ;
292+ } catch {
293+ rejectOnce ( fail ( ) ) ;
294+ return ;
295+ }
296+ readiness . then ( readyFrame => {
297+ if ( settled ) return ;
298+ if ( closed || readyFrame !== frame ) {
299+ rejectOnce ( trackingError ( ) ) ;
300+ return ;
301+ }
302+ requestFrame = readyFrame ;
303+ pending . set ( message . id , {
304+ resolve : resolveOnce ,
305+ reject : rejectOnce ,
306+ timer,
307+ type,
308+ frame : readyFrame ,
309+ recoverUnavailable : requestOptions . recoverUnavailable === true ,
310+ ...payload ,
311+ } ) ;
312+ try {
313+ readyFrame . contentWindow . postMessage ( message , bridge . origin ) ;
314+ } catch {
315+ pending . delete ( message . id ) ;
316+ dropFailedFrame ( readyFrame ) ;
317+ rejectOnce ( fail ( ) ) ;
318+ }
319+ } , ( ) => {
320+ if ( ! settled ) rejectOnce ( fail ( ) ) ;
321+ } ) ;
222322 } ) ;
223323 } ;
224324
225325 const resume = ( ) => {
226- if ( ! closed && frame && currentDocument . visibilityState !== 'hidden' ) {
326+ if ( ! closed && currentDocument . visibilityState !== 'hidden' ) {
227327 void client . read ( ) . catch ( ( ) => notify ( ) ) ;
228328 }
229329 } ;
@@ -258,10 +358,16 @@ export function createSharedTrackingClient(options = {}) {
258358 notify ( ) ;
259359 }
260360 try {
261- await request ( 'choose' , { choice, expectedRevision } ) ;
361+ await request (
362+ 'choose' ,
363+ { choice, expectedRevision } ,
364+ { recoverUnavailable : choice === 'accepted' } ,
365+ ) ;
262366 } catch ( error ) {
263367 if ( choice === 'rejected' ) discardFrame ( ) ;
264- throw error ;
368+ if ( choice !== 'accepted' || ! recoverableFailures . has ( error ) ) throw error ;
369+ await request ( 'read' ) ;
370+ if ( effectiveState ( ) . choice !== 'accepted' ) throw fail ( ) ;
265371 }
266372 // A refusal that could not reach the bridge remains local and visible as
267373 // an error, rather than allowing an older shared acceptance to return.
0 commit comments