Skip to content

Commit 780aeb4

Browse files
committed
The User Executor will add new records after create
* Push subscription IDs and onesignal IDs can be added to the new records storage after they are created * Requests can be delayed by a specific “cool down” period plus a small buffer - they are flushed after a delay when they need to wait for the "cool down" period to access a user or subscription after its creation. * Anytime `prepareForExecution()` fails, the executor will put a delayed task to flush the requests again again after a specific delay. * In CreateUser, new IDs will only be added to the new records storage if the Creates were truly Creates, and not “faux” Creates meant to get an onesignal ID for a given external ID. This latter case happens after a 409 Identify conflict, meaning the user definitely exists and the Onesignal ID will not be new. * In IdentifyUser successful, the executor adds onesignal ID to new records again because an immediate fetch for hydration may not return the newly-applied external ID. We will encounter a problem with hydration in this case. * User fetch will always be called after a delay unless it is to refresh the user state on a new session. This is because a fetch that is not on a new session is always for getting user data after a create or identify. * Nits: Also moved some opening braces to new lines for improved readability of multi-line “if / let / guard” statements
1 parent dad646b commit 780aeb4

File tree

1 file changed

+92
-50
lines changed

1 file changed

+92
-50
lines changed

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,21 @@ import OneSignalOSCore
3535
*/
3636
class OSUserExecutor {
3737
var userRequestQueue: [OSUserRequest] = []
38+
private let newRecordsState: OSNewRecordsState
39+
/// Delay by the "cool down" period plus a buffer of a set amount of milliseconds
40+
private let flushDelayMilliseconds = Int(OP_REPO_POST_CREATE_DELAY_SECONDS * 1_000 + 200) // TODO: This could come from a config, plist, method, remote params
3841

39-
// The User executor dispatch queue, serial. This synchronizes access to the request queues.
42+
/// The User executor dispatch queue, serial. This synchronizes access to the request queues.
4043
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSUserExecutor", target: .global())
4144

42-
init() {
45+
init(newRecordsState: OSNewRecordsState) {
46+
self.newRecordsState = newRecordsState
4347
uncacheUserRequests()
4448
migrateTransferSubscriptionRequests()
4549
executePendingRequests()
4650
}
4751

48-
// Read in requests from the cache, do not read in FetchUser requests as this is not needed.
52+
/// Read in requests from the cache, do not read in FetchUser requests as this is not needed.
4953
private func uncacheUserRequests() {
5054
var userRequestQueue: [OSUserRequest] = []
5155

@@ -90,7 +94,7 @@ class OSUserExecutor {
9094
// 3. Both models don't exist yet
9195
// Drop the request if the identityModelToIdentify does not already exist AND the request is missing OSID
9296
// Otherwise, this request will forever fail `prepareForExecution` and block pending requests such as recovery calls to `logout` or `login`
93-
guard request.prepareForExecution() else {
97+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
9498
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor.start() dropped: \(request)")
9599
continue
96100
}
@@ -143,37 +147,47 @@ class OSUserExecutor {
143147
}
144148
}
145149

146-
func executePendingRequests() {
147-
self.dispatchQueue.async {
148-
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequests called with queue \(self.userRequestQueue)")
150+
/**
151+
Requests are flushed after a delay when they need to wait for the "cool down" period to access a user or subscription after its creation.
152+
*/
153+
func executePendingRequests(withDelay: Bool = false) {
154+
if withDelay {
155+
self.dispatchQueue.asyncAfter(deadline: .now() + .milliseconds(flushDelayMilliseconds)) { [weak self] in
156+
self?._executePendingRequests()
157+
}
158+
} else {
159+
self.dispatchQueue.async {
160+
self._executePendingRequests()
161+
}
162+
}
163+
}
164+
165+
private func _executePendingRequests() {
166+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequests called with queue \(self.userRequestQueue)")
149167

150-
if self.userRequestQueue.isEmpty {
168+
for request in self.userRequestQueue {
169+
// Return as soon as we reach an un-executable request
170+
guard request.prepareForExecution(newRecordsState: self.newRecordsState)
171+
else {
172+
OneSignalLog.onesignalLog(.LL_WARN, message: "OSUserExecutor.executePendingRequests() is blocked by unexecutable request \(request)")
173+
executePendingRequests(withDelay: true)
151174
return
152175
}
153176

154-
for request in self.userRequestQueue {
155-
// Return as soon as we reach an un-executable request
156-
if !request.prepareForExecution() {
157-
OneSignalLog.onesignalLog(.LL_WARN, message: "OSUserExecutor.executePendingRequests() is blocked by unexecutable request \(request)")
158-
return
159-
}
160-
161-
if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let fetchIdentityRequest = request as? OSRequestFetchIdentityBySubscription {
162-
self.executeFetchIdentityBySubscriptionRequest(fetchIdentityRequest)
163-
return
164-
} else if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser {
165-
self.executeCreateUserRequest(createUserRequest)
166-
return
167-
} else if request.isKind(of: OSRequestIdentifyUser.self), let identifyUserRequest = request as? OSRequestIdentifyUser {
168-
self.executeIdentifyUserRequest(identifyUserRequest)
169-
return
170-
} else if request.isKind(of: OSRequestFetchUser.self), let fetchUserRequest = request as? OSRequestFetchUser {
171-
self.executeFetchUserRequest(fetchUserRequest)
172-
return
173-
} else {
174-
// Log Error
175-
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.")
176-
}
177+
if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let fetchIdentityRequest = request as? OSRequestFetchIdentityBySubscription {
178+
self.executeFetchIdentityBySubscriptionRequest(fetchIdentityRequest)
179+
return
180+
} else if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser {
181+
self.executeCreateUserRequest(createUserRequest)
182+
return
183+
} else if request.isKind(of: OSRequestIdentifyUser.self), let identifyUserRequest = request as? OSRequestIdentifyUser {
184+
self.executeIdentifyUserRequest(identifyUserRequest)
185+
return
186+
} else if request.isKind(of: OSRequestFetchUser.self), let fetchUserRequest = request as? OSRequestFetchUser {
187+
self.executeFetchUserRequest(fetchUserRequest)
188+
return
189+
} else {
190+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.")
177191
}
178192
}
179193
}
@@ -204,11 +218,6 @@ extension OSUserExecutor {
204218
guard !request.sentToClient else {
205219
return
206220
}
207-
guard request.prepareForExecution() else {
208-
// Currently there are no requirements needed before sending this request, so this will set the path
209-
return
210-
}
211-
request.sentToClient = true
212221

213222
// Hook up push subscription model if exists, it may be updated with a subscription_id, etc.
214223
if let modelId = request.pushSubscriptionModel?.modelId,
@@ -217,17 +226,29 @@ extension OSUserExecutor {
217226
request.updatePushSubscriptionModel(pushSubscriptionModel)
218227
}
219228

229+
guard request.prepareForExecution(newRecordsState: newRecordsState)
230+
else {
231+
executePendingRequests(withDelay: true)
232+
return
233+
}
234+
235+
request.sentToClient = true
236+
220237
OneSignalCoreImpl.sharedClient().execute(request) { response in
221238
self.removeFromQueue(request)
222239

223-
// TODO: Differentiate if we need to fetch the user based on response code of 200, 201, 202
224240
// Create User's response won't send us the user's complete info if this user already exists
225241
if let response = response {
242+
let shouldAddNewRecords = request.pushSubscriptionModel != nil
226243
// Parse the response for any data we need to update
227-
self.parseFetchUserResponse(response: response, identityModel: request.identityModel, originalPushToken: request.originalPushToken)
244+
self.parseFetchUserResponse(
245+
response: response,
246+
identityModel: request.identityModel,
247+
originalPushToken: request.originalPushToken,
248+
addNewRecords: shouldAddNewRecords
249+
)
228250

229251
// If this user already exists and we logged into an external_id, fetch the user data
230-
// TODO: Only do this if response code is 200 or 202
231252
// Fetch the user only if its the current user and non-anonymous
232253
if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel),
233254
let identity = request.parameters?["identity"] as? [String: String],
@@ -270,9 +291,13 @@ extension OSUserExecutor {
270291
guard !request.sentToClient else {
271292
return
272293
}
273-
guard request.prepareForExecution() else {
294+
295+
// newRecordsState is unused for this request
296+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
297+
executePendingRequests(withDelay: true)
274298
return
275299
}
300+
276301
request.sentToClient = true
277302

278303
OneSignalCoreImpl.sharedClient().execute(request) { response in
@@ -322,10 +347,12 @@ extension OSUserExecutor {
322347
guard !request.sentToClient else {
323348
return
324349
}
325-
guard request.prepareForExecution() else {
326-
// Missing onesignal_id
350+
351+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
352+
executePendingRequests(withDelay: true)
327353
return
328354
}
355+
329356
request.sentToClient = true
330357

331358
OneSignalCoreImpl.sharedClient().execute(request) { _ in
@@ -345,8 +372,9 @@ extension OSUserExecutor {
345372
request.identityModelToUpdate.hydrate(aliases)
346373

347374
// the anonymous user has been identified, still need to Fetch User as we cleared local data
348-
// Fetch the user only if its the current user
349375
if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) {
376+
// Add onesignal ID to new records because an immediate fetch may not return the newly-applied external ID
377+
self.newRecordsState.add(onesignalId, true)
350378
self.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModelToUpdate)
351379
} else {
352380
self.executePendingRequests()
@@ -394,18 +422,22 @@ extension OSUserExecutor {
394422

395423
appendToQueue(request)
396424

397-
executePendingRequests()
425+
// User fetch will always be called after a delay unless it is to refresh the user state on a new session
426+
executePendingRequests(withDelay: !onNewSession)
398427
}
399428

400429
func executeFetchUserRequest(_ request: OSRequestFetchUser) {
401430
guard !request.sentToClient else {
402431
return
403432
}
404-
guard request.prepareForExecution() else {
405-
// This should not happen as we set the alias to use for the request path
433+
434+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
435+
executePendingRequests(withDelay: true)
406436
return
407437
}
438+
408439
request.sentToClient = true
440+
409441
OneSignalCoreImpl.sharedClient().execute(request) { response in
410442
self.removeFromQueue(request)
411443

@@ -465,12 +497,15 @@ extension OSUserExecutor {
465497
/**
466498
Used to parse Create User and Fetch User responses. The `originalPushToken` is the push token when the request was created, which may be different from the push token currently in the SDK. For example, when the request was created, there may be no push token yet, but soon after, the SDK receives a push token. This is used to determine whether or not to hydrate the push subscription.
467499
*/
468-
func parseFetchUserResponse(response: [AnyHashable: Any], identityModel: OSIdentityModel, originalPushToken: String?) {
500+
func parseFetchUserResponse(response: [AnyHashable: Any], identityModel: OSIdentityModel, originalPushToken: String?, addNewRecords: Bool = false) {
469501

470502
// If this was a create user, it hydrates the onesignal_id of the request's identityModel
471503
// The model in the store may be different, and it may be waiting on the onesignal_id of this previous model
472504
if let identityObject = parseIdentityObjectResponse(response) {
473505
identityModel.hydrate(identityObject)
506+
if addNewRecords, let onesignalId = identityObject[OS_ONESIGNAL_ID] {
507+
newRecordsState.add(onesignalId)
508+
}
474509
}
475510

476511
// TODO: Determine how to hydrate the push subscription, which is still faulty.
@@ -480,13 +515,19 @@ extension OSUserExecutor {
480515

481516
// Hydrate the push subscription if we don't already have a subscription ID AND token matches the original request
482517
if OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId == nil,
483-
let subscriptionObject = parseSubscriptionObjectResponse(response) {
518+
let subscriptionObject = parseSubscriptionObjectResponse(response)
519+
{
484520
for subModel in subscriptionObject {
485521
if subModel["type"] as? String == "iOSPush",
486-
areTokensEqual(tokenA: originalPushToken, tokenB: subModel["token"] as? String) { // response may have "" token or no token
522+
// response may have "" token or no token
523+
areTokensEqual(tokenA: originalPushToken, tokenB: subModel["token"] as? String)
524+
{
487525
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.hydrate(subModel)
488526
if let subId = subModel["id"] as? String {
489527
OSNotificationsManager.setPushSubscriptionId(subId)
528+
if addNewRecords {
529+
newRecordsState.add(subId)
530+
}
490531
}
491532
break
492533
}
@@ -510,7 +551,8 @@ extension OSUserExecutor {
510551
if let address = subModel["token"] as? String,
511552
let rawType = subModel["type"] as? String,
512553
rawType != "iOSPush",
513-
let type = OSSubscriptionType(rawValue: rawType) {
554+
let type = OSSubscriptionType(rawValue: rawType)
555+
{
514556
if let model = models[address] {
515557
// This subscription exists in the store, hydrate
516558
model.hydrate(subModel)

0 commit comments

Comments
 (0)