diff --git a/package.json b/package.json index 3e2c3f6b5..850770d66 100644 --- a/package.json +++ b/package.json @@ -77,12 +77,12 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "42.4 kB", + "limit": "42.6 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "12.354 kB", + "limit": "12.55 kB", "gzip": true }, { diff --git a/src/shared/database/client.ts b/src/shared/database/client.ts index c6db46299..4db95d23c 100644 --- a/src/shared/database/client.ts +++ b/src/shared/database/client.ts @@ -93,6 +93,36 @@ export const getDb = (version = VERSION) => { return dbPromise; }; +// On iOS Safari PWA after a push subscription, `readwrite` requests on the +// `Options` object store can stall indefinitely (no success/error/abort), +// hanging `OneSignal.init()` until WebKit's watchdog aborts the transaction +// (~30 min). Other stores and reads are unaffected, and reopening the DB +// doesn't help. Cap Options writes with a short timeout and resolve undefined +// on stall; the dropped values are session metadata the SW reads with sensible +// fallbacks. Once any write times out we trip a page-scoped circuit breaker +// so the remaining ~7 Options writes during init short-circuit instead of +// each paying the full timeout. Remove if WebKit ever fixes the underlying +// bug: https://bugs.webkit.org/show_bug.cgi?id=315804 +let optionsWriteWedged = false; +export const isOptionsWriteWedged = () => optionsWriteWedged; + +function guardOptionsWrite( + storeName: IDBStoreName, + op: () => Promise, +): Promise { + if (storeName !== 'Options') return op(); + if (optionsWriteWedged) return Promise.resolve(undefined); + let timer: ReturnType | undefined; + const timeout = new Promise((resolve) => { + timer = setTimeout(() => { + optionsWriteWedged = true; + Log._warn(`db.${storeName} timed out`); + resolve(undefined); + }, 2000); + }); + return Promise.race([op(), timeout]).finally(() => clearTimeout(timer)); +} + // Export db object with the same API as before export const db = { async get( @@ -105,10 +135,12 @@ export const db = { return (await dbPromise).getAll(storeName); }, async put(storeName: K, value: IndexedDBSchema[K]['value']) { - return (await dbPromise).put(storeName, value); + const _db = await dbPromise; + return guardOptionsWrite(storeName, () => _db.put(storeName, value)); }, async delete(storeName: K, key: IndexedDBSchema[K]['key']) { - return (await dbPromise).delete(storeName, key); + const _db = await dbPromise; + return guardOptionsWrite(storeName, () => _db.delete(storeName, key)); }, }; diff --git a/src/shared/helpers/init.ts b/src/shared/helpers/init.ts index 20bffda99..1fd9e54ff 100755 --- a/src/shared/helpers/init.ts +++ b/src/shared/helpers/init.ts @@ -3,7 +3,7 @@ import { ModelChangeTags } from 'src/core/types/models'; import Bell from '../../page/bell/Bell'; import type { AppConfig } from '../config/types'; import type { ContextInterface } from '../context/types'; -import { db, getIdsValue } from '../database/client'; +import { db, getIdsValue, isOptionsWriteWedged } from '../database/client'; import { getSubscription, setSubscription } from '../database/subscription'; import type { OptionKey } from '../database/types'; import Log from '../libraries/Log'; @@ -352,6 +352,12 @@ export async function initSaveState(overridingPageTitle?: string) { await db.put('Options', { key: 'lastPushId', value: null }); await db.put('Options', { key: 'lastPushToken', value: null }); await db.put('Options', { key: 'lastOptedIn', value: null }); + // Bail out if the Options reset got circuit-broken. Committing the new + // appId now would strand the previous app's metadata under it, and the + // `previousAppId !== appId` gate above would keep us out of this branch + // on later loads — leaving the stale values permanent. Skipping the + // appId commit instead lets a future non-wedged load complete the reset. + if (isOptionsWriteWedged()) return; await db.put('Ids', { type: 'registrationId', id: null }); await db.put('Ids', { type: 'userId', id: null }); OneSignal._coreDirector._subscriptionModelStore._clear(ModelChangeTags._Hydrate);