sitepong 0.2.8 → 0.2.9
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.
- package/dist/entries/rn.d.ts +20 -0
- package/dist/entries/rn.js +1341 -1187
- package/dist/entries/rn.js.map +1 -1
- package/ios/WatchtowerCore/StructuralRecorder.swift +64 -5
- package/package.json +1 -1
|
@@ -31,6 +31,14 @@ public final class SitePongStructuralCapture {
|
|
|
31
31
|
private var flushTimer: Timer?
|
|
32
32
|
private var running = false
|
|
33
33
|
|
|
34
|
+
// Incremental delivery: count of events CONFIRMED stored (== next seq_start).
|
|
35
|
+
// The device ships only the delta each flush and clears its buffer, so memory
|
|
36
|
+
// stays bounded and the uplink never re-sends the whole session. Node ids
|
|
37
|
+
// (`ids`/`nextId`) stay PERSISTENT across flushes — the server stitches the
|
|
38
|
+
// deltas by id, so resetting them would corrupt the tree.
|
|
39
|
+
private var flushedEventCount = 0
|
|
40
|
+
private var sending = false
|
|
41
|
+
|
|
34
42
|
private func now() -> Int { Int(Date().timeIntervalSince(startedAt) * 1000) }
|
|
35
43
|
|
|
36
44
|
/// Start capturing. `endpoint` is the ingest base URL (e.g.
|
|
@@ -57,6 +65,17 @@ public final class SitePongStructuralCapture {
|
|
|
57
65
|
}
|
|
58
66
|
sampleTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in self?.sample() }
|
|
59
67
|
flushTimer = Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true) { [weak self] _ in self?.flush() }
|
|
68
|
+
|
|
69
|
+
// Flush on background so the tail isn't stranded when timers suspend.
|
|
70
|
+
NotificationCenter.default.addObserver(
|
|
71
|
+
self, selector: #selector(onBackground),
|
|
72
|
+
name: UIApplication.didEnterBackgroundNotification, object: nil
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@objc private func onBackground() {
|
|
77
|
+
guard running else { return }
|
|
78
|
+
flush()
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
public func setScreen(_ name: String) {
|
|
@@ -71,6 +90,9 @@ public final class SitePongStructuralCapture {
|
|
|
71
90
|
running = false
|
|
72
91
|
sampleTimer?.invalidate(); sampleTimer = nil
|
|
73
92
|
flushTimer?.invalidate(); flushTimer = nil
|
|
93
|
+
NotificationCenter.default.removeObserver(
|
|
94
|
+
self, name: UIApplication.didEnterBackgroundNotification, object: nil
|
|
95
|
+
)
|
|
74
96
|
flush()
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -171,23 +193,60 @@ public final class SitePongStructuralCapture {
|
|
|
171
193
|
return v
|
|
172
194
|
}
|
|
173
195
|
|
|
196
|
+
/// Ship the pending event delta (only what's accumulated since the last
|
|
197
|
+
/// confirmed flush), then clear the device buffer. The batch carries its
|
|
198
|
+
/// `seq_start` so the server dedups retries; a failed batch is re-queued so
|
|
199
|
+
/// nothing is lost offline. All buffer mutation stays on the main thread.
|
|
174
200
|
private func flush() {
|
|
175
|
-
guard !endpoint.isEmpty, !events.isEmpty, let w = keyWindow() else { return }
|
|
201
|
+
guard !endpoint.isEmpty, !sending, !events.isEmpty, let w = keyWindow() else { return }
|
|
202
|
+
let batch = events
|
|
203
|
+
let seqStart = flushedEventCount
|
|
204
|
+
events.removeAll(keepingCapacity: true) // drain the device buffer
|
|
205
|
+
sending = true
|
|
206
|
+
post(batch: batch, seqStart: seqStart,
|
|
207
|
+
viewport: (Int(w.bounds.width), Int(w.bounds.height)), attempt: 0)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private func post(batch: [[String: Any]], seqStart: Int, viewport: (Int, Int), attempt: Int) {
|
|
176
211
|
let payload: [String: Any] = [
|
|
177
212
|
"session_id": sessionId,
|
|
178
213
|
"platform": platform,
|
|
179
|
-
"viewport": ["w":
|
|
180
|
-
"
|
|
214
|
+
"viewport": ["w": viewport.0, "h": viewport.1],
|
|
215
|
+
"seq_start": seqStart,
|
|
216
|
+
"events": batch,
|
|
181
217
|
]
|
|
182
218
|
guard let body = try? JSONSerialization.data(withJSONObject: payload),
|
|
183
|
-
let url = URL(string: "\(endpoint)/api/replay-stream/native") else {
|
|
219
|
+
let url = URL(string: "\(endpoint)/api/replay-stream/native") else {
|
|
220
|
+
DispatchQueue.main.async { self.sending = false }
|
|
221
|
+
return
|
|
222
|
+
}
|
|
184
223
|
var req = URLRequest(url: url)
|
|
185
224
|
req.httpMethod = "POST"
|
|
186
225
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
187
226
|
req.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
|
|
188
227
|
if let pid = projectId { req.setValue(pid, forHTTPHeaderField: "X-Project-ID") }
|
|
189
228
|
req.httpBody = body
|
|
190
|
-
|
|
229
|
+
|
|
230
|
+
URLSession.shared.dataTask(with: req) { [weak self] _, resp, err in
|
|
231
|
+
guard let self = self else { return }
|
|
232
|
+
let ok = (resp as? HTTPURLResponse).map { (200..<300).contains($0.statusCode) } ?? false
|
|
233
|
+
DispatchQueue.main.async {
|
|
234
|
+
if ok && err == nil {
|
|
235
|
+
self.flushedEventCount = seqStart + batch.count
|
|
236
|
+
self.sending = false
|
|
237
|
+
} else if attempt < 3 {
|
|
238
|
+
let delay = pow(2.0, Double(attempt)) // 1,2,4s
|
|
239
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
240
|
+
self.post(batch: batch, seqStart: seqStart, viewport: viewport, attempt: attempt + 1)
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
// Give up on this attempt: re-queue at the front so the seq
|
|
244
|
+
// stays contiguous and the delta re-sends on the next flush.
|
|
245
|
+
self.events.insert(contentsOf: batch, at: 0)
|
|
246
|
+
self.sending = false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}.resume()
|
|
191
250
|
}
|
|
192
251
|
}
|
|
193
252
|
|