@apex-inc/capacitor-plugin 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -67,6 +67,12 @@ public final class NativeBatchSender {
|
|
|
67
67
|
/// Drains the queue, one batch at a time, with exponential backoff
|
|
68
68
|
/// on retryable failures. Safe to call concurrently — only one
|
|
69
69
|
/// flush actually runs at a time; concurrent callers no-op.
|
|
70
|
+
///
|
|
71
|
+
/// Accumulator is passed by value through the recursion (no inout).
|
|
72
|
+
/// The previous inout-based design tripped Swift's exclusivity
|
|
73
|
+
/// runtime check at line 85 because the same `totalFlushed` local
|
|
74
|
+
/// was both captured by the `done` closure and passed inout — a
|
|
75
|
+
/// classic simultaneous-access violation that traps in debug builds.
|
|
70
76
|
public func flush(queue: NativeOfflineQueue, completion: @escaping (Int, Int) -> Void) {
|
|
71
77
|
serial.async { [weak self] in
|
|
72
78
|
guard let self = self else { completion(0, 0); return }
|
|
@@ -77,54 +83,56 @@ public final class NativeBatchSender {
|
|
|
77
83
|
return
|
|
78
84
|
}
|
|
79
85
|
self.inFlight = true
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
self.drainLoop(queue: queue, accumulated: 0) { [weak self] flushed in
|
|
87
|
+
self?.serial.async {
|
|
88
|
+
self?.inFlight = false
|
|
89
|
+
let remaining = (try? queue.size()) ?? 0
|
|
90
|
+
completion(flushed, remaining)
|
|
91
|
+
}
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/// Recursive drain. `accumulated` is the running total of successfully
|
|
97
|
+
/// flushed events; `done` is invoked exactly once with the final count.
|
|
90
98
|
private func drainLoop(
|
|
91
99
|
queue: NativeOfflineQueue,
|
|
92
|
-
|
|
93
|
-
done: @escaping () -> Void
|
|
100
|
+
accumulated: Int,
|
|
101
|
+
done: @escaping (Int) -> Void
|
|
94
102
|
) {
|
|
95
103
|
let batch: [NativeQueuedEvent]
|
|
96
104
|
do {
|
|
97
105
|
batch = try queue.peek(batchSize: batchSize)
|
|
98
106
|
} catch {
|
|
99
107
|
if debug { print("[apex-capacitor] peek failed: \(error)") }
|
|
100
|
-
done()
|
|
108
|
+
done(accumulated)
|
|
101
109
|
return
|
|
102
110
|
}
|
|
103
111
|
if batch.isEmpty {
|
|
104
|
-
done()
|
|
112
|
+
done(accumulated)
|
|
105
113
|
return
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
let localFlushed = flushed
|
|
109
116
|
sendBatchWithRetry(batch, attempt: 0) { [weak self] outcome in
|
|
110
|
-
guard let self = self else { done(); return }
|
|
117
|
+
guard let self = self else { done(accumulated); return }
|
|
111
118
|
self.serial.async {
|
|
112
119
|
let ids = batch.map { $0.id }
|
|
113
120
|
switch outcome {
|
|
114
121
|
case .success:
|
|
115
122
|
try? queue.markSent(ids: ids)
|
|
116
123
|
if self.debug { print("[apex-capacitor] flushed batch of \(batch.count)") }
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
self.drainLoop(
|
|
125
|
+
queue: queue,
|
|
126
|
+
accumulated: accumulated + batch.count,
|
|
127
|
+
done: done
|
|
128
|
+
)
|
|
121
129
|
case .nonRetryable(let status):
|
|
122
130
|
if self.debug { print("[apex-capacitor] non-retryable HTTP \(status); leaving events queued") }
|
|
123
|
-
done()
|
|
131
|
+
done(accumulated)
|
|
124
132
|
case .retryableExhausted(let err):
|
|
125
133
|
try? queue.markFailed(ids: ids)
|
|
126
134
|
if self.debug { print("[apex-capacitor] retries exhausted: \(err)") }
|
|
127
|
-
done()
|
|
135
|
+
done(accumulated)
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
}
|
|
@@ -76,6 +76,11 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
76
76
|
// reached the server. See friction-log F6.
|
|
77
77
|
private var batchSender: NativeBatchSender?
|
|
78
78
|
private var flushTimer: Timer?
|
|
79
|
+
// MMP-207 — debounced flush. Without this, every `track()` call
|
|
80
|
+
// kicked a POST, so 8 quick taps = 8 separate single-event HTTP
|
|
81
|
+
// requests. With debounce, rapid-fire events coalesce into one
|
|
82
|
+
// batch on the next tick.
|
|
83
|
+
private var flushDebounceWork: DispatchWorkItem?
|
|
79
84
|
|
|
80
85
|
public override func load() {
|
|
81
86
|
let defaults = UserDefaults.standard
|
|
@@ -220,16 +225,29 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
|
|
223
|
-
/// Fire-and-forget flush.
|
|
224
|
-
///
|
|
228
|
+
/// Fire-and-forget flush. Track-triggered calls are debounced
|
|
229
|
+
/// 250ms so rapid tap sessions coalesce into a single batch POST.
|
|
230
|
+
/// Timer + foreground + initialize callers bypass the debounce
|
|
231
|
+
/// since they're already paced.
|
|
225
232
|
private func kickFlush(reason: String) {
|
|
226
233
|
guard let queue = offlineQueue, let sender = batchSender else { return }
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if self
|
|
230
|
-
|
|
234
|
+
let runFlush: () -> Void = { [weak self] in
|
|
235
|
+
guard let self = self else { return }
|
|
236
|
+
if self.debug { print("[apex-capacitor] flush kicked (\(reason))") }
|
|
237
|
+
sender.flush(queue: queue) { [weak self] flushed, remaining in
|
|
238
|
+
if self?.debug == true && (flushed > 0 || remaining > 0) {
|
|
239
|
+
print("[apex-capacitor] flush done — sent \(flushed), remaining \(remaining)")
|
|
240
|
+
}
|
|
231
241
|
}
|
|
232
242
|
}
|
|
243
|
+
if reason == "track" {
|
|
244
|
+
flushDebounceWork?.cancel()
|
|
245
|
+
let work = DispatchWorkItem(block: runFlush)
|
|
246
|
+
flushDebounceWork = work
|
|
247
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(250), execute: work)
|
|
248
|
+
} else {
|
|
249
|
+
runFlush()
|
|
250
|
+
}
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
// MARK: - ATT
|
|
@@ -424,6 +442,14 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
|
|
|
424
442
|
}
|
|
425
443
|
codablePayload["platform"] = AnyCodable("ios")
|
|
426
444
|
codablePayload["timestamp"] = AnyCodable(ISO8601DateFormatter().string(from: Date()))
|
|
445
|
+
// MMP-207 — stamp visitorId. Without this, /api/events accepts
|
|
446
|
+
// the event into the SDK firehose store but its second pass
|
|
447
|
+
// bails at `if (!visitorId) continue;` so the event never
|
|
448
|
+
// reaches the TESTEVT# / EVT# stores the debug dashboard reads.
|
|
449
|
+
// Net result before this fix: events arrived but the dashboard
|
|
450
|
+
// showed nothing. The visitor id is the stable per-install id
|
|
451
|
+
// we mint in load() and persist to UserDefaults.
|
|
452
|
+
codablePayload["visitorId"] = AnyCodable(visitorId)
|
|
427
453
|
let event = NativeQueuedEvent(
|
|
428
454
|
id: id,
|
|
429
455
|
payload: codablePayload,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apex-inc/capacitor-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Apex Capacitor plugin — iOS/Android attribution, events, deep linking, SKAN, and offline-tolerant tracking for Capacitor apps.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|