Repository navigation
feat: [SDK-5242] wait for init in login and logout - #1529
sherwinski wants to merge 2 commits into
Conversation
A direct call to login or logout before init dereferenced the user model that init creates. The OneSignalDeferred array runs init before later callbacks, but a direct window.OneSignal.login() can come first. A boolean OneSignal._coreReady turns true once init created the user model and loaded the stored consent. While the flag is false, the 2 methods wait for init when init runs, as the Android SDK does. A call before init throws "Must call 'init' before '<method>'", because a wait inside the OneSignalDeferred array ahead of init would never end. When init stops or throws before the user model, the finally block wakes the waiters without the flag and the call logs a warning. The consent check runs after the wait, because the init config and the stored consent are not loaded before that point. A call after init stays synchronous, so the operations of the calls that follow it without an await keep their order in the queue.
The init wait in login and logout adds 171 B gzip to the page bundle. The worker bundle is unchanged.
d77720d to
c75ae8e
Compare
There was a problem hiding this comment.
Multi-model review
Independent reviews from Claude Opus 5.5, GPT 5.6 Sol, and Grok 4.7. The wait / throw-before-init / post-wait consent sequencing looks sound and well tested. After-init stays synchronous as intended.
Act on
- Class static block (Opus).
static {}inOneSignal.tsis the first one insrc/. The page bundle targetses2022and does not downlevel syntax; static blocks parse only in Safari 16.4+ (Chrome 94+, Firefox 93+). Safari 16.0–16.3 still loads the ES6 bundle viaisPushNotificationsSupported(). A field initializer (static _coreReadyPromise = new Promise(...)) avoids the parse break and still assigns_resolveCoreReadywithout a later field define resetting it.
Consider
- Failed-init and consent-blocked calls resolve successfully (Opus + Grok).
_awaitCorereturnsfalseand the public methods returnundefined. That matches the PR write-up, butawait OneSignal.login(...)then looks like a successful login and anupdateUserJwttoken is dropped. Same quiet return after the post-wait consent check. If this is the contract, a sentence on the public methods would help; otherwise reject when init will never complete. User.*after a waitingloginstill no-ops (Grok). While init is in progress,OneSignal.Useris still the placeholdernew UserNamespace(false).login(); User.addTag(...)without await drops the tag; login later succeeds. Out of this PR’s stated scope, but the new wait makes that pattern look safe when it is not.
Noted / dismissed
- GPT 5.6 Sol reported no findings.
- Settling
_coreReadybefore_delayedInit/initSaveStateis intentional (user model + consent, not full session init). The app-id-change race is real but narrow. - Tests stub
_settleCoreReady/_resolveCoreReadyinstead of a real early-returninit; the call-order test does not recordupdateUserJwtin the sequence. Neither blocks merge. _pendingInitstartingtrueis pre-existing.
Sent by Cursor Automation: PR Reviews
| } | ||
|
|
||
| static Notifications = new NotificationsNamespace(); | ||
| static Slidedown = new SlidedownNamespace(); |
There was a problem hiding this comment.
Act on (Opus): This is the first class static block in src/. The page bundle is target: 'es2022' and terser does not lower syntax, so this ships as written. Static blocks parse only in Safari 16.4+; Safari 16.0–16.3 still loads OneSignalSDK.page.es6.js via isPushNotificationsSupported() and would SyntaxError the whole SDK.
A field initializer keeps the same “no later field define can reset the resolver” property:
static _coreReadyPromise = new Promise<void>((resolve) => {
OneSignal._resolveCoreReady = resolve;
});| @@ -154,8 +195,10 @@ export default class OneSignal { | |||
| if (!idb) return; | |||
There was a problem hiding this comment.
Consider (Opus + Grok): After a stopped or thrown init, waiters wake with _coreReady still false and the public method resolves successfully. Same quiet return after the post-wait consent check. Documented in the PR, but await login() / await updateUserJwt() cannot tell a real success from a skip, and a retry cannot wait — errorIfInitAlreadyCalled blocks a second init, and a settled resolver ignores another call. Worth rejecting when init will never complete, and leaving only the consent path as a non-throwing return.
init in login and logout
abdulraqeeb33
left a comment
There was a problem hiding this comment.
Login waits only while init is already running, and a login ahead of init throws, so the deferred array still reaches init. Stored consent is loaded before that wait ends. The static block does not parse on Safari versions that still receive this bundle.
| // `declare` emits no field define, so nothing can run after the block and reset these. | ||
| declare static _coreReadyPromise: Promise<void>; | ||
| declare static _resolveCoreReady: () => void; | ||
| static { |
There was a problem hiding this comment.
static { } is a parse error on Safari 16.0 through 16.3. Those versions still load OneSignalSDK.page.es6.js through the legacy push check, and target: 'es2022' does not lower this. The rest of this file already runs there. A field initializer keeps the resolver:
static _coreReadyPromise = new Promise<void>((resolve) => {
OneSignal._resolveCoreReady = resolve;
});
fadi-george
left a comment
There was a problem hiding this comment.
Unsure about having this change
Yeah, I need to think this one through a bit more. Will revisit after all of the other Web work is done. |


Description
1 Line Summary
loginandlogoutwait for init while init runs, throw before init, then check consent.Details
Opened against
mainfor the team to decide on. The pre-init window is older than Identity Verification, and nothing in the IV branch depends on this wait. The IV branch addsupdateUserJwt, which needs the same guard; that comes with the merge ofmainintofeat/identity-verification.Problem. A direct call to
loginorlogoutbeforeinitdereferencesOneSignal._coreDirector, which init creates, and fails with an undefined dereference. TheOneSignalDeferredarray runs init before later callbacks, but a directwindow.OneSignal.login()can come first.Fix. A boolean
OneSignal._coreReadyturns true once init created the user model and loaded the stored consent. The 2 methods call_awaitCore(method)only while the flag is false:_coreReadyPromise, with a debug log. Calls that wait resume in call order, because each one takes the same number of microtask hops.Must call 'init' before '<method>', as on Android (OneSignalImp.kt:531-608). A wait would never end when the call runs inside theOneSignalDeferredarray ahead of init, because the array runs its callbacks one at a time. The deferred loop logs the error and moves on to init.finallyininitwakes the waiters, the flag stays false, and the call logs<method> skipped: init did not completeand returns.isConsentRequiredButNotGiven(). The check before the wait runs only when the flag is true, becauseOneSignal._consentGivenis not loaded before init reads the stored consent.Open question for the team. Throw or wait before
init? Android throwsIllegalStateExceptionwhen init was not started and waits when init runs. This PR follows Android. The other option, a wait with no end, blocks theOneSignalDeferredloop when alogincallback is pushed ahead ofinit.The after-init path stays synchronous. The guard reads the flag before any
await. Callers rely onOneSignal.login(id); OneSignal.User.addTag(...)withoutawaitto enqueue the login operation first, and the existing testshould preserve operations order without needing awaitpins that.History: #1513 was the first version, on the IV stack. The review there asked for a ready flag set after the consent load, ready only on init success, and a pinned order between a waiting call and the calls after it. All 3 are in this version.
Follow-up outside this repo: document the rule in the SDK reference (mintlify-docs).
Bundle size
Measured with size-limit against
main(79f0f525):OneSignalSDK.page.es6.jsOneSignalSDK.sw.jsThe last commit raises the page limit to 43.30 kB.
Systems Affected
Validation
Tests
Info
vp test run— 63 files, 568 tests pass.vp check— 0 errors, 5 pre-existing warnings.vp run build:prod— passes with the raised limit.Tests in
OneSignal.test.ts, blockOneSignal - Before Init:loginandlogoutthrow when init was not calledloginandlogoutwait for init, then run, with a debug logloginobeys a consent requirement that the init config carrieslogindoes not read the consent state before init loads itloginis skipped with a warning when init stopped before the user modelTest in
pageSdkInit3.test.tswith a realinit: aloginahead ofinitinOneSignalDeferredfails with the init error, init still runs, aloginthat starts during init proceeds once init loads the stored consent, and it runs before aloginmade afterawait init.Checklist
Programming Checklist
Interfaces:
Functions:
Typescript:
Other:
elem of arraysyntax. PreferforEachor usemapcontextif possible. Instead, we can pass it to function/constructor so that we don't callOneSignal.contextScreenshots
Info
Not needed: no UI change.
Checklist
Related Tickets
initinlogin,logout, andupdateUserJwt#1513