@@ -93,38 +93,34 @@ export const getDb = (version = VERSION) => {
9393 return dbPromise ;
9494} ;
9595
96- // On iOS Safari PWA after a push subscription, `readwrite` requests on the
97- // `Options` object store can stall indefinitely (no success/error/abort).
98- // Other stores and reads are unaffected, and reopening the DB doesn't help.
99- // Without this guard, `OneSignal.init()` hangs until WebKit's watchdog
100- // eventually aborts the transaction (~30 minutes). Workaround: cap Options
101- // writes with a short timeout, then trip a page-scoped circuit breaker so
102- // subsequent writes short-circuit. The values that fail to persist are
103- // session metadata the SW reads with sensible fallbacks. Remove if WebKit
104- // ever fixes the underlying bug: https://bugs.webkit.org/show_bug.cgi?id=315804
105- const OPTIONS_WRITE_TIMEOUT_MS = 1500 ;
106- let optionsWriteWedged = false ;
107-
108- export const isOptionsWriteWedged = ( ) => optionsWriteWedged ;
96+ // On iOS Safari PWA after a push subscription, a `readwrite` request can stall
97+ // indefinitely (no success/error/abort). Our timeout makes the JS promise
98+ // resolve, but the underlying IndexedDB transaction stays open and blocks every
99+ // later operation queued behind it on that object store -- including reads. So
100+ // guarding writes alone isn't enough: once a write wedges, the next read of the
101+ // same store (e.g. Options) hangs too, stalling `OneSignal.init()` until
102+ // WebKit's watchdog aborts the txn (~30 minutes). Workaround: cap every op with
103+ // a short timeout, trip a page-scoped circuit breaker on the first stall, then
104+ // short-circuit all subsequent ops (reads included). Dropped writes are session
105+ // metadata the SW re-derives or idempotent queued operations retried next load;
106+ // dropped reads fall back to the in-memory model state hydrated before the
107+ // wedge. Remove if WebKit ever fixes it: https://bugs.webkit.org/show_bug.cgi?id=315804
108+ const DB_TIMEOUT_MS = 1500 ;
109+ let dbWedged = false ;
110+
111+ export const isReadwriteWedged = ( ) => dbWedged ;
109112
110113// `op` is invoked synchronously (callers await `dbPromise` first), so the
111- // timeout scopes only to the readwrite request, not DB open/upgrade. Once a
112- // write times out we trip a page-scoped circuit breaker so the rest of init's
113- // Options writes short-circuit instead of each paying the full timeout.
114- function guardOptionsWrite < T > (
115- storeName : IDBStoreName ,
116- label : string ,
117- op : ( ) => Promise < T > ,
118- ) : Promise < T | undefined > {
119- if ( storeName !== 'Options' ) return op ( ) ;
120- if ( optionsWriteWedged ) return Promise . resolve ( undefined ) ;
114+ // timeout scopes only to the request, not DB open/upgrade.
115+ function guard < T > ( label : string , op : ( ) => Promise < T > , fallback : T ) : Promise < T > {
116+ if ( dbWedged ) return Promise . resolve ( fallback ) ;
121117 let timer : ReturnType < typeof setTimeout > ;
122- const timeout = new Promise < undefined > ( ( resolve ) => {
118+ const timeout = new Promise < T > ( ( resolve ) => {
123119 timer = setTimeout ( ( ) => {
124- optionsWriteWedged = true ;
120+ dbWedged = true ;
125121 Log . _warn ( `db.${ label } timed out` ) ;
126- resolve ( undefined ) ;
127- } , OPTIONS_WRITE_TIMEOUT_MS ) ;
122+ resolve ( fallback ) ;
123+ } , DB_TIMEOUT_MS ) ;
128124 } ) ;
129125 return Promise . race ( [ op ( ) , timeout ] ) . finally ( ( ) => clearTimeout ( timer ) ) ;
130126}
@@ -134,25 +130,30 @@ export const db = {
134130 storeName : K ,
135131 key : IndexedDBSchema [ K ] [ 'key' ] ,
136132 ) : Promise < IndexedDBSchema [ K ] [ 'value' ] | undefined > {
137- return ( await dbPromise ) . get ( storeName , key ) ;
133+ const _db = await dbPromise ;
134+ return guard ( `get(${ storeName } )` , ( ) => _db . get ( storeName , key ) , undefined ) ;
138135 } ,
139136 async getAll < K extends IDBStoreName > ( storeName : K ) : Promise < IndexedDBSchema [ K ] [ 'value' ] [ ] > {
140- return ( await dbPromise ) . getAll ( storeName ) ;
137+ const _db = await dbPromise ;
138+ return guard < IndexedDBSchema [ K ] [ 'value' ] [ ] > (
139+ `getAll(${ storeName } )` ,
140+ ( ) => _db . getAll ( storeName ) ,
141+ [ ] ,
142+ ) ;
141143 } ,
142144 async put < K extends IDBStoreName > ( storeName : K , value : IndexedDBSchema [ K ] [ 'value' ] ) {
143145 const _db = await dbPromise ;
144- return guardOptionsWrite ( storeName , `put(${ storeName } )` , ( ) => _db . put ( storeName , value ) ) ;
146+ return guard ( `put(${ storeName } )` , ( ) => _db . put ( storeName , value ) , undefined ) ;
145147 } ,
146148 async delete < K extends IDBStoreName > ( storeName : K , key : IndexedDBSchema [ K ] [ 'key' ] ) {
147149 const _db = await dbPromise ;
148- return guardOptionsWrite ( storeName , `delete(${ storeName } /${ key } )` , ( ) =>
149- _db . delete ( storeName , key ) ,
150- ) ;
150+ return guard ( `delete(${ storeName } /${ key } )` , ( ) => _db . delete ( storeName , key ) , undefined ) ;
151151 } ,
152152} ;
153153
154154export const clearStore = async < K extends IDBStoreName > ( storeName : K ) => {
155- return ( await dbPromise ) . clear ( storeName ) ;
155+ const _db = await dbPromise ;
156+ return guard ( `clear(${ storeName } )` , ( ) => _db . clear ( storeName ) , undefined ) ;
156157} ;
157158
158159export const getObjectStoreNames = async ( ) => {
0 commit comments