Skip to content

Commit 9620ffc

Browse files
committed
Add mutex protection to checkAndTriggerSubscriptionChanged
Motivation: there is a race condition where it is possible for the app state to be the same when this function fires twice. Even though the function is async, we need it to run synchronously to ensure that each operation is fully completed before the next begins, thus preserving the integrity and consistency of the application state.
1 parent 2c80351 commit 9620ffc

File tree

2 files changed

+42
-21
lines changed

2 files changed

+42
-21
lines changed

src/helpers/EventHelper.ts

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,52 @@ import LocalStorage from '../utils/LocalStorage';
1111
import { CustomLinkManager } from '../managers/CustomLinkManager';
1212

1313
export default class EventHelper {
14-
static onNotificationPermissionChange() {
15-
EventHelper.checkAndTriggerSubscriptionChanged();
14+
static _mutexPromise: Promise<void> = Promise.resolve();
15+
static _mutexLocked = false;
16+
17+
static async onNotificationPermissionChange() {
18+
await EventHelper.checkAndTriggerSubscriptionChanged();
1619
}
1720

1821
static async onInternalSubscriptionSet(optedOut: boolean) {
1922
LimitStore.put('subscription.optedOut', optedOut);
2023
}
2124

2225
static async checkAndTriggerSubscriptionChanged() {
23-
OneSignalUtils.logMethodCall('checkAndTriggerSubscriptionChanged');
24-
const context: ContextSWInterface = OneSignal.context;
25-
const subscriptionState = await context.subscriptionManager.getSubscriptionState();
26-
const isPushEnabled = await OneSignal.privateIsPushNotificationsEnabled();
27-
const appState = await Database.getAppState();
28-
const { lastKnownPushEnabled } = appState;
29-
const didStateChange = (
30-
lastKnownPushEnabled === null ||
31-
isPushEnabled !== lastKnownPushEnabled
32-
);
33-
if (!didStateChange) return;
34-
Log.info(
35-
`The user's subscription state changed from ` +
36-
`${lastKnownPushEnabled === null ? '(not stored)' : lastKnownPushEnabled}${subscriptionState.subscribed}`
37-
);
38-
LocalStorage.setIsPushNotificationsEnabled(isPushEnabled);
39-
appState.lastKnownPushEnabled = isPushEnabled;
40-
await Database.setAppState(appState);
41-
EventHelper.triggerSubscriptionChanged(isPushEnabled);
26+
if (EventHelper._mutexLocked) {
27+
await EventHelper._mutexPromise;
28+
}
29+
30+
EventHelper._mutexLocked = true;
31+
// eslint-disable-next-line no-async-promise-executor
32+
EventHelper._mutexPromise = new Promise(async (resolve, reject) => {
33+
try {
34+
OneSignalUtils.logMethodCall('checkAndTriggerSubscriptionChanged');
35+
const context: ContextSWInterface = OneSignal.context;
36+
const subscriptionState = await context.subscriptionManager.getSubscriptionState();
37+
const isPushEnabled = await OneSignal.privateIsPushNotificationsEnabled();
38+
const appState = await Database.getAppState();
39+
const { lastKnownPushEnabled } = appState;
40+
const didStateChange = (
41+
lastKnownPushEnabled === null ||
42+
isPushEnabled !== lastKnownPushEnabled
43+
);
44+
if (!didStateChange) return;
45+
Log.info(
46+
`The user's subscription state changed from ` +
47+
`${lastKnownPushEnabled === null ? '(not stored)' : lastKnownPushEnabled}${subscriptionState.subscribed}`
48+
);
49+
LocalStorage.setIsPushNotificationsEnabled(isPushEnabled);
50+
appState.lastKnownPushEnabled = isPushEnabled;
51+
await Database.setAppState(appState);
52+
EventHelper.triggerSubscriptionChanged(isPushEnabled);
53+
EventHelper._mutexLocked = false;
54+
resolve();
55+
} catch (e) {
56+
EventHelper._mutexLocked = false;
57+
reject(`checkAndTriggerSubscriptionChanged error: ${e}`);
58+
}
59+
});
4260
}
4361

4462
static async _onSubscriptionChanged(newSubscriptionState: boolean | undefined) {

test/unit/public-sdk-apis/onSession.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { createSubscription } from "../../support/tester/utils";
1919
import EventsTestHelper from '../../support/tester/EventsTestHelper';
2020
import { DelayedPromptType } from '../../../src/models/Prompts';
21+
import EventHelper from "../../../src/helpers/EventHelper";
2122

2223

2324
const sinonSandbox: SinonSandbox = sinon.sandbox.create();
@@ -30,6 +31,8 @@ test.afterEach(function (_t: ExecutionContext) {
3031
OneSignal._initCalled = false;
3132
OneSignal.__initAlreadyCalled = false;
3233
OneSignal._sessionInitAlreadyRunning = false;
34+
EventHelper._mutexPromise = Promise.resolve();
35+
EventHelper._mutexLocked = false;
3336
});
3437

3538
/**

0 commit comments

Comments
 (0)