Skip to content

Commit 713bdf2

Browse files
committed
Create executor to handle custom events
1 parent b59b360 commit 713bdf2

File tree

1 file changed

+249
-0
lines changed

1 file changed

+249
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2025 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import OneSignalOSCore
29+
import OneSignalCore
30+
31+
class OSCustomEventsExecutor: OSOperationExecutor {
32+
private enum EventConstants {
33+
static let name = "name"
34+
static let onesignalId = "onesignal_id"
35+
static let timestamp = "timestamp"
36+
static let payload = "payload"
37+
static let deviceType = "device_type"
38+
static let sdk = "sdk"
39+
static let appVersion = "app_version"
40+
static let type = "type"
41+
static let deviceModel = "device_model"
42+
static let deviceOs = "device_os"
43+
static let osSdk = "os_sdk"
44+
static let ios = "ios"
45+
static let iOSPush = "iOSPush"
46+
}
47+
48+
var supportedDeltas: [String] = [OS_CUSTOM_EVENT_DELTA]
49+
private var deltaQueue: [OSDelta] = []
50+
private var requestQueue: [OSRequestCustomEvents] = []
51+
private let newRecordsState: OSNewRecordsState
52+
53+
// The executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `requestQueue`.
54+
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSCustomEventsExecutor", target: .global())
55+
56+
init(newRecordsState: OSNewRecordsState) {
57+
self.newRecordsState = newRecordsState
58+
// Read unfinished deltas and requests from cache, if any...
59+
uncacheDeltas()
60+
uncacheRequests()
61+
}
62+
63+
private func uncacheDeltas() {
64+
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
65+
for (index, delta) in deltaQueue.enumerated().reversed() {
66+
if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil {
67+
// The identity model does not exist, drop this Delta
68+
OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(delta)")
69+
deltaQueue.remove(at: index)
70+
}
71+
}
72+
self.deltaQueue = deltaQueue
73+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
74+
} else {
75+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor error encountered reading from cache for \(OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY)")
76+
self.deltaQueue = []
77+
}
78+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor successfully uncached Deltas: \(deltaQueue)")
79+
}
80+
81+
private func uncacheRequests() {
82+
if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCustomEvents] {
83+
// Hook each uncached Request to the model in the store
84+
for (index, request) in requestQueue.enumerated().reversed() {
85+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
86+
// 1. The identity model exist in the repo, set it to be the Request's model
87+
request.identityModel = identityModel
88+
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
89+
// 2. The request can be sent, add the model to the repo
90+
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
91+
} else {
92+
// 3. The identitymodel do not exist AND this request cannot be sent, drop this Request
93+
OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(request)")
94+
requestQueue.remove(at: index)
95+
}
96+
}
97+
self.requestQueue = requestQueue
98+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
99+
} else {
100+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor error encountered reading from cache for \(OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY)")
101+
self.requestQueue = []
102+
}
103+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor successfully uncached Requests: \(requestQueue)")
104+
}
105+
106+
func enqueueDelta(_ delta: OSDelta) {
107+
self.dispatchQueue.async {
108+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor enqueue delta \(delta)")
109+
self.deltaQueue.append(delta)
110+
}
111+
}
112+
113+
func cacheDeltaQueue() {
114+
self.dispatchQueue.async {
115+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
116+
}
117+
}
118+
119+
/// The `deltaQueue` can contain events for multiple users. They will remain as Deltas if there is no onesignal ID yet for its user.
120+
func processDeltaQueue(inBackground: Bool) {
121+
self.dispatchQueue.async {
122+
if self.deltaQueue.isEmpty {
123+
// Delta queue is empty but there may be pending requests
124+
self.processRequestQueue(inBackground: inBackground)
125+
return
126+
}
127+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor processDeltaQueue with queue: \(self.deltaQueue)")
128+
129+
// Holds mapping of identity model ID to the events for it
130+
var combinedEvents: [String: [[String: Any]]] = [:]
131+
132+
// 1. Combine the events for every distinct user
133+
for (index, delta) in self.deltaQueue.enumerated().reversed() {
134+
guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId),
135+
let onesignalId = identityModel.onesignalId
136+
else {
137+
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor.processDeltaQueue skipping: \(delta)")
138+
// keep this Delta in the queue, as it is not yet ready to be processed
139+
continue
140+
}
141+
142+
guard let properties = delta.value as? [String: Any] else {
143+
// This should not happen as there are preventative typing measures before this step
144+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor.processDeltaQueue dropped due to invalid properties: \(delta)")
145+
self.deltaQueue.remove(at: index)
146+
continue
147+
}
148+
149+
let event: [String: Any] = [
150+
EventConstants.name: delta.property,
151+
EventConstants.onesignalId: onesignalId,
152+
EventConstants.timestamp: ISO8601DateFormatter().string(from: delta.timestamp),
153+
EventConstants.payload: self.addSdkMetadata(properties: properties)
154+
]
155+
156+
combinedEvents[identityModel.modelId, default: []].append(event)
157+
self.deltaQueue.remove(at: index)
158+
}
159+
160+
// 2. Turn each user's events into a Request
161+
for (modelId, events) in combinedEvents {
162+
guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(modelId)
163+
else {
164+
// This should never happen as we already checked this during Deltas processing above
165+
continue
166+
}
167+
let request = OSRequestCustomEvents(
168+
events: events,
169+
identityModel: identityModel
170+
)
171+
self.requestQueue.append(request)
172+
}
173+
174+
// Persist executor's requests (including new request) to storage
175+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
176+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
177+
178+
self.processRequestQueue(inBackground: inBackground)
179+
}
180+
}
181+
182+
/**
183+
Adds additional data about the SDK to the event payload.
184+
*/
185+
private func addSdkMetadata(properties: [String: Any]) -> [String: Any] {
186+
// TODO: Exact information contained in payload should be confirmed before the custom events GA release
187+
let metadata = [
188+
EventConstants.deviceType: EventConstants.ios,
189+
EventConstants.sdk: ONESIGNAL_VERSION,
190+
EventConstants.appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
191+
EventConstants.type: EventConstants.iOSPush,
192+
EventConstants.deviceModel: OSDeviceUtils.getDeviceVariant(),
193+
EventConstants.deviceOs: UIDevice.current.systemVersion
194+
]
195+
var payload = properties
196+
payload[EventConstants.osSdk] = metadata
197+
return payload
198+
}
199+
200+
/// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue.
201+
private func processRequestQueue(inBackground: Bool) {
202+
if requestQueue.isEmpty {
203+
return
204+
}
205+
206+
for request in requestQueue {
207+
executeRequest(request, inBackground: inBackground)
208+
}
209+
}
210+
211+
private func executeRequest(_ request: OSRequestCustomEvents, inBackground: Bool) {
212+
guard !request.sentToClient else {
213+
return
214+
}
215+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
216+
return
217+
}
218+
request.sentToClient = true
219+
220+
let backgroundTaskIdentifier = CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK + UUID().uuidString
221+
if inBackground {
222+
OSBackgroundTaskManager.beginBackgroundTask(backgroundTaskIdentifier)
223+
}
224+
225+
OneSignalCoreImpl.sharedClient().execute(request) { _ in
226+
self.dispatchQueue.async {
227+
self.requestQueue.removeAll(where: { $0 == request})
228+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
229+
if inBackground {
230+
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
231+
}
232+
}
233+
} onFailure: { error in
234+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor request failed with error: \(error.debugDescription)")
235+
self.dispatchQueue.async {
236+
let responseType = OSNetworkingUtils.getResponseStatusType(error.code)
237+
if responseType != .retryable {
238+
// Fail, no retry, remove from cache and queue
239+
self.requestQueue.removeAll(where: { $0 == request})
240+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
241+
}
242+
// TODO: Handle payload too large (not necessary for alpha release)
243+
if inBackground {
244+
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
245+
}
246+
}
247+
}
248+
}
249+
}

0 commit comments

Comments
 (0)