Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
fix: [SDK-4336] add circuit breaker and defer appId commit on wedge #1469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
fix: [SDK-4336] add circuit breaker and defer appId commit on wedge #1469
Changes from all commits
7f5df21ea843d9File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
Check failure on line 363 in src/shared/helpers/init.ts
Wedge bail skips in-memory subscription store clear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 The early-return on
isOptionsWriteWedged()at init.ts:360 sits between the fourOptionsresets and theIds.{registrationId,userId}+_subscriptionModelStore._clear(_Hydrate)calls, so on a wedged app-ID switch the in-memory subscription store (andIds.userId) keeps the previous app's identity for the rest of the page session. Downstream_sendOnSessionUpdateand outcome requests then serialize previous-app subscription IDs against the new app's config — the exact cross-app contamination this PR exists to prevent, just bounded to the wedged session. Moving_subscriptionModelStore._clear(ModelChangeTags._Hydrate)above line 360 closes the gap; the clear is synchronous in memory and its fire-and-forgetdb.deletetargets thesubscriptionsstore, not the wedgedOptionsstore.Extended reasoning...
What the bug is\n\nThe new bail on line 360 (
if (isOptionsWriteWedged()) return;) is placed between the fourOptionsresets and three subsequent statements that are also part of the reset:\n\nts\nawait db.put('Options', { key: 'lastOptedIn', value: null });\nif (isOptionsWriteWedged()) return; // ← new bail\nawait db.put('Ids', { type: 'registrationId', id: null });\nawait db.put('Ids', { type: 'userId', id: null });\nOneSignal._coreDirector._subscriptionModelStore._clear(ModelChangeTags._Hydrate);\n\n\nThe PR comment above the bail only justifies skipping theIds.appIdcommit further down ("Committing the new appId now would strand the previous app's metadata under it"). It does not address that the bail also drops the in-memory_subscriptionModelStore._clear()— a call that was unconditional pre-PR and that operates on a different IDB store entirely (subscriptions, notOptions), so it cannot itself wedge.\n\n## Why this is a regression (and the concrete harm path)\n\nPre-PR, the wedged app-switch path was slow but functionally correct: the fourOptionswrites each timed out (~8s), execution continued, and_subscriptionModelStore._clear(_Hydrate)ran. The in-memory store was emptied, so_getPushSubscriptionModel()returnedundefinedandUpdateManager._sendOnSessionUpdateshort-circuited via thesubscriptionModel?._notification_types !== Subscribedguard.\n\nPost-PR, after the first 2s timeout trips the breaker, the bail skips_clear(). TheSubscriptionModelStorewas populated atCoreModuleconstruction viaSimpleModelStore._load()(SimpleModelStore.ts:22), so the previous app's hydratedpushSubscriptionmodel survives in memory. The chain is then:\n\n1.Ids.userIdis not nulled (skipped by the same bail), sogetSubscription().deviceId(subscription.ts) and_isAlreadyRegisteredWithOneSignal()(base.ts:114-117) still return the previous app's identity.\n2.onSdkInitialized→_sendOnSessionUpdatereads_getPushSubscriptionModel()and gets the previous app's model.\n3. The on_session / outcome requests serializesubscription: { id: pushSubscriptionModel.id, ... }against the new app'sgetAppId()endpoints.\n\nThat is the same cross-app contamination this PR exists to fix, just bounded to the wedged session rather than persisted.\n\n## Step-by-step proof\n\n1. iOS Safari PWA wedgesOptionswrites (the documented WebKit bug 315804).\n2. User switchesappIdfrom A → B and reloads.previousAppIdinIdsis still A, so we enter thepreviousAppId !== appIdbranch at init.ts:351.\n3. Firstdb.put('Options', { key: 'isPushEnabled', value: null })waits 2s, hits the timeout —optionsWriteWedgedflips true. The remaining threeOptionswrites short-circuit viaif (optionsWriteWedged) return Promise.resolve(undefined).\n4. Line 360 returns.Ids.userId/Ids.registrationIdare still A's;_subscriptionModelStorestill holds A'spushSubscriptionrehydrated from IDB duringnew CoreModule().\n5. Init continues.onSdkInitialized→_isAlreadyRegisteredWithOneSignal()reads A'sIds.userIdand returns true.\n6._sendOnSessionUpdate→_getPushSubscriptionModelreturns A's model. Withnotification_types === Subscribed,_upsertSession(SessionOrigin._UserNewSession)fires withOneSignal.configholding B's appId but A's subscription id in the body.\n7.SDK_INITIALIZED_PUBLICconsumers readingOneSignal.User.PushSubscriptionsee A's id on a page running as B.\n\n## Addressing the "intentional design" refutations\n\nThe refutations argue this is an intentional "defer everything on wedge" policy that keeps the wedged session in a coherent "previous-app" state. Three reasons that framing doesn't hold:\n\n- The PR comment only documents the appId-commit deferral. It is silent on the_clear()and the twoIdsresets, which are the statements actually being collateral-damaged. The comment scope is precisely "don't strand previous-app Options metadata under new appId" — moving_clear()up does not violate that.\n- The "partial-reset would create a different inconsistency" objection cuts the other way. The wedged session is already inconsistent:OneSignal.configis the new app's, but in-memory_subscriptionModelStoreandIds.userIdare the old app's. Letting_sendOnSessionUpdatefire with a populated old-app subscription model is precisely the failure mode this PR is supposed to prevent. Letting_clear()run does not affectOptionsrows at all (it targets thesubscriptionsIDB store) andModelStoreListenerearly-returns for any tag other than_Normal(ModelStoreListener.ts:55-58), so no spurious operations get enqueued.\n- The "could trigger duplicate player#create" worry depends onhandleAutoResubscribebeing entered with cleared state. But that path already gates onautoResubscribe && currentPermission === 'granted', and_isAlreadyRegisteredWithOneSignalstill reflects A'sIds.userId(which is also skipped by the same bail — moving_clear()doesn't change that). If anything, the bigger and more principled fix is to move all three skipped statements (Ids.registrationId,Ids.userId, and_clear()) above the bail, since none of them areOptionswrites and none can wedge.\n\n## Suggested fix\n\nMove the in-memory_clear()(and ideally the twoIdsresets, which are also unguarded byguardOptionsWrite) above the early-return:\n\nts\nawait db.put('Options', { key: 'lastOptedIn', value: null });\nawait db.put('Ids', { type: 'registrationId', id: null });\nawait db.put('Ids', { type: 'userId', id: null });\nOneSignal._coreDirector._subscriptionModelStore._clear(ModelChangeTags._Hydrate);\nif (isOptionsWriteWedged()) return;\n\n\nThis preserves the PR's stated intent (don't commitIds.appIdon wedge, so a future non-wedged load completes the reset) while preventing the wedged session from emitting on_session / outcome traffic that mixes previous-app subscription identity with new-app endpoints.Uh oh!
There was an error while loading. Please reload this page.