@rejourneyco/react-native 1.0.12 → 1.0.14
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/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +16 -7
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +5 -1
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +29 -6
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +34 -1
- package/ios/Engine/RejourneyImpl.swift +12 -3
- package/ios/Recording/ReplayOrchestrator.swift +5 -1
- package/ios/Recording/TelemetryPipeline.swift +27 -2
- package/ios/Recording/VisualCapture.swift +29 -1
- package/lib/commonjs/index.js +1 -1
- package/lib/module/index.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
|
@@ -60,6 +60,7 @@ import kotlin.concurrent.withLock
|
|
|
60
60
|
*/
|
|
61
61
|
sealed class SessionState {
|
|
62
62
|
object Idle : SessionState()
|
|
63
|
+
data class Starting(val sessionId: String, val startTimeMs: Long) : SessionState()
|
|
63
64
|
data class Active(val sessionId: String, val startTimeMs: Long) : SessionState()
|
|
64
65
|
data class Paused(val sessionId: String, val startTimeMs: Long) : SessionState()
|
|
65
66
|
object Terminated : SessionState()
|
|
@@ -488,9 +489,16 @@ class RejourneyModuleImpl(
|
|
|
488
489
|
// Check if already active
|
|
489
490
|
stateLock.withLock {
|
|
490
491
|
val currentState = state
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
492
|
+
when (currentState) {
|
|
493
|
+
is SessionState.Active -> {
|
|
494
|
+
promise.resolve(createResultMap(true, currentState.sessionId))
|
|
495
|
+
return@post
|
|
496
|
+
}
|
|
497
|
+
is SessionState.Starting -> {
|
|
498
|
+
promise.resolve(createResultMap(true, currentState.sessionId))
|
|
499
|
+
return@post
|
|
500
|
+
}
|
|
501
|
+
else -> Unit
|
|
494
502
|
}
|
|
495
503
|
}
|
|
496
504
|
|
|
@@ -522,10 +530,10 @@ class RejourneyModuleImpl(
|
|
|
522
530
|
DiagnosticLog.fault("[Rejourney] CRITICAL: No current activity available for capture!")
|
|
523
531
|
}
|
|
524
532
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
533
|
+
val pendingSessionId = "session_${System.currentTimeMillis()}_${java.util.UUID.randomUUID().toString().replace("-", "").lowercase()}"
|
|
534
|
+
stateLock.withLock {
|
|
535
|
+
state = SessionState.Starting(pendingSessionId, System.currentTimeMillis())
|
|
536
|
+
}
|
|
529
537
|
|
|
530
538
|
// Begin replay
|
|
531
539
|
ReplayOrchestrator.shared?.beginReplay(
|
|
@@ -539,6 +547,7 @@ class RejourneyModuleImpl(
|
|
|
539
547
|
|
|
540
548
|
// Allow orchestrator time to spin up
|
|
541
549
|
mainHandler.postDelayed({
|
|
550
|
+
val sid = ReplayOrchestrator.shared?.replayId ?: pendingSessionId
|
|
542
551
|
stateLock.withLock {
|
|
543
552
|
state = SessionState.Active(sid, System.currentTimeMillis())
|
|
544
553
|
}
|
|
@@ -279,10 +279,14 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
279
279
|
)
|
|
280
280
|
val queueDepthAtFinalize = TelemetryPipeline.shared?.getQueueDepth() ?: 0
|
|
281
281
|
|
|
282
|
+
// Capture the current generation so a stale halt posted here won't
|
|
283
|
+
// stop a new session's capture that starts before this block runs.
|
|
284
|
+
val haltGeneration = VisualCapture.shared?.captureGeneration ?: -1
|
|
285
|
+
|
|
282
286
|
// Do local teardown immediately so lifecycle rollover never depends on network latency.
|
|
283
287
|
mainHandler.post {
|
|
284
288
|
TelemetryPipeline.shared?.shutdown()
|
|
285
|
-
VisualCapture.shared?.halt()
|
|
289
|
+
VisualCapture.shared?.halt(haltGeneration)
|
|
286
290
|
InteractionRecorder.shared?.deactivate()
|
|
287
291
|
StabilityMonitor.shared?.deactivate()
|
|
288
292
|
AnrSentinel.shared?.deactivate()
|
|
@@ -159,9 +159,12 @@ class TelemetryPipeline private constructor(private val context: Context) {
|
|
|
159
159
|
fun shutdown() {
|
|
160
160
|
heartbeatRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
161
161
|
heartbeatRunnable = null
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
SegmentDispatcher.shared.halt()
|
|
164
164
|
appSuspending()
|
|
165
|
+
|
|
166
|
+
// Clear pending frame bundles so they don't leak into the next session
|
|
167
|
+
frameQueue.clear()
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
fun finalizeAndShip() {
|
|
@@ -181,9 +184,12 @@ class TelemetryPipeline private constructor(private val context: Context) {
|
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
fun submitFrameBundle(payload: ByteArray, filename: String, startMs: Long, endMs: Long, frameCount: Int) {
|
|
184
|
-
|
|
187
|
+
// Capture the session ID now so frames are always attributed to the
|
|
188
|
+
// session that was active when they were captured, not when they ship.
|
|
189
|
+
val capturedSessionId = currentReplayId
|
|
190
|
+
DiagnosticLog.trace("[TelemetryPipeline] submitFrameBundle: $frameCount frames, ${payload.size} bytes, deferredMode=$deferredMode, session=$capturedSessionId")
|
|
185
191
|
serialWorker.execute {
|
|
186
|
-
val bundle = PendingFrameBundle(filename, payload, startMs, endMs, frameCount)
|
|
192
|
+
val bundle = PendingFrameBundle(filename, payload, startMs, endMs, frameCount, capturedSessionId)
|
|
187
193
|
frameQueue.enqueue(bundle)
|
|
188
194
|
if (!deferredMode) shipPendingFrames()
|
|
189
195
|
}
|
|
@@ -233,11 +239,23 @@ class TelemetryPipeline private constructor(private val context: Context) {
|
|
|
233
239
|
DiagnosticLog.trace("[TelemetryPipeline] shipPendingFrames: no frames in queue")
|
|
234
240
|
return
|
|
235
241
|
}
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
|
|
243
|
+
// Determine which session these frames belong to. Prefer the session ID
|
|
244
|
+
// captured at enqueue time; fall back to the current active session.
|
|
245
|
+
val targetSession = next.sessionId ?: currentReplayId
|
|
246
|
+
if (targetSession == null) {
|
|
247
|
+
DiagnosticLog.caution("[TelemetryPipeline] shipPendingFrames: no session ID, requeueing")
|
|
238
248
|
frameQueue.requeue(next)
|
|
239
249
|
return
|
|
240
250
|
}
|
|
251
|
+
|
|
252
|
+
// Drop frames that belong to a session that is no longer active —
|
|
253
|
+
// they would be uploaded under the wrong session and cause flickering.
|
|
254
|
+
if (next.sessionId != null && next.sessionId != currentReplayId && currentReplayId != null) {
|
|
255
|
+
DiagnosticLog.trace("[TelemetryPipeline] shipPendingFrames: dropping ${next.count} stale frames from session ${next.sessionId} (current=${currentReplayId})")
|
|
256
|
+
serialWorker.execute { shipPendingFrames() }
|
|
257
|
+
return
|
|
258
|
+
}
|
|
241
259
|
|
|
242
260
|
DiagnosticLog.trace("[TelemetryPipeline] shipPendingFrames: transmitting ${next.count} frames to SegmentDispatcher")
|
|
243
261
|
|
|
@@ -641,7 +659,8 @@ private data class PendingFrameBundle(
|
|
|
641
659
|
val payload: ByteArray,
|
|
642
660
|
val rangeStart: Long,
|
|
643
661
|
val rangeEnd: Long,
|
|
644
|
-
val count: Int
|
|
662
|
+
val count: Int,
|
|
663
|
+
val sessionId: String? = null
|
|
645
664
|
)
|
|
646
665
|
|
|
647
666
|
private class FrameBundleQueue(private val maxPending: Int) {
|
|
@@ -671,4 +690,8 @@ private class FrameBundleQueue(private val maxPending: Int) {
|
|
|
671
690
|
}
|
|
672
691
|
|
|
673
692
|
fun size(): Int = queue.size
|
|
693
|
+
|
|
694
|
+
fun clear() {
|
|
695
|
+
lock.withLock { queue.clear() }
|
|
696
|
+
}
|
|
674
697
|
}
|
|
@@ -79,6 +79,8 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
79
79
|
private var deferredUntilCommit = false
|
|
80
80
|
private var framesDiskPath: File? = null
|
|
81
81
|
private var currentSessionId: String? = null
|
|
82
|
+
@Volatile var captureGeneration: Int = 0
|
|
83
|
+
private set
|
|
82
84
|
|
|
83
85
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
84
86
|
|
|
@@ -103,10 +105,35 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
103
105
|
|
|
104
106
|
fun beginCapture(sessionOrigin: Long) {
|
|
105
107
|
DiagnosticLog.trace("[VisualCapture] beginCapture called, currentActivity=${currentActivity?.get()?.javaClass?.simpleName ?: "null"}, state=${stateMachine.currentState}")
|
|
108
|
+
|
|
109
|
+
// If we're still in CAPTURING state (halt() from previous session hasn't
|
|
110
|
+
// run yet due to async mainHandler.post), force-halt first to prevent the
|
|
111
|
+
// stale halt from stopping the new session's capture later.
|
|
112
|
+
if (stateMachine.currentState == CaptureState.CAPTURING) {
|
|
113
|
+
DiagnosticLog.trace("[VisualCapture] Force-halting stale capture before starting new session")
|
|
114
|
+
stopCaptureTimer()
|
|
115
|
+
stateMachine.transition(CaptureState.HALTED)
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
if (!stateMachine.transition(CaptureState.CAPTURING)) {
|
|
107
119
|
DiagnosticLog.trace("[VisualCapture] beginCapture REJECTED - state transition failed from ${stateMachine.currentState}")
|
|
108
120
|
return
|
|
109
121
|
}
|
|
122
|
+
|
|
123
|
+
// Bump generation so any stale halt() posted by the previous session
|
|
124
|
+
// (via mainHandler.post) becomes a no-op and doesn't stop this capture.
|
|
125
|
+
captureGeneration++
|
|
126
|
+
|
|
127
|
+
// Discard any frames left over from a previous session to prevent
|
|
128
|
+
// cross-session frame leakage (frames from session A appearing in session B).
|
|
129
|
+
stateLock.withLock {
|
|
130
|
+
val staleCount = screenshots.size
|
|
131
|
+
if (staleCount > 0) {
|
|
132
|
+
DiagnosticLog.trace("[VisualCapture] Clearing $staleCount stale frames from previous session")
|
|
133
|
+
screenshots.clear()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
110
137
|
sessionEpoch = sessionOrigin
|
|
111
138
|
frameCounter.set(0)
|
|
112
139
|
|
|
@@ -122,7 +149,13 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
122
149
|
startCaptureTimer()
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
fun halt() {
|
|
152
|
+
fun halt(expectedGeneration: Int = -1) {
|
|
153
|
+
// If a specific generation is expected (async/posted halt from a previous
|
|
154
|
+
// session), skip if a new session has already started capture.
|
|
155
|
+
if (expectedGeneration >= 0 && expectedGeneration != captureGeneration) {
|
|
156
|
+
DiagnosticLog.trace("[VisualCapture] Skipping stale halt (gen=$expectedGeneration, current=$captureGeneration)")
|
|
157
|
+
return
|
|
158
|
+
}
|
|
126
159
|
if (!stateMachine.transition(CaptureState.HALTED)) return
|
|
127
160
|
stopCaptureTimer()
|
|
128
161
|
|
|
@@ -27,6 +27,7 @@ public final class RejourneyImpl: NSObject {
|
|
|
27
27
|
|
|
28
28
|
private enum SessionState {
|
|
29
29
|
case idle
|
|
30
|
+
case starting(sessionId: String, startTime: TimeInterval)
|
|
30
31
|
case active(sessionId: String, startTime: TimeInterval)
|
|
31
32
|
case paused(sessionId: String, startTime: TimeInterval)
|
|
32
33
|
case terminated
|
|
@@ -353,12 +354,14 @@ public final class RejourneyImpl: NSObject {
|
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
self.stateLock.lock()
|
|
356
|
-
|
|
357
|
+
switch self.state {
|
|
358
|
+
case .active(let sid, _), .starting(let sid, _):
|
|
357
359
|
self.stateLock.unlock()
|
|
358
360
|
resolve(["success": true, "sessionId": sid])
|
|
359
361
|
return
|
|
362
|
+
default:
|
|
363
|
+
self.stateLock.unlock()
|
|
360
364
|
}
|
|
361
|
-
self.stateLock.unlock()
|
|
362
365
|
|
|
363
366
|
if !userId.isEmpty && userId != "anonymous" && !userId.hasPrefix("anon_") {
|
|
364
367
|
self.currentUserIdentity = userId
|
|
@@ -376,11 +379,17 @@ public final class RejourneyImpl: NSObject {
|
|
|
376
379
|
// Activate native network interception
|
|
377
380
|
RejourneyURLProtocol.enable()
|
|
378
381
|
|
|
382
|
+
let pendingSessionId = "session_\(Int(Date().timeIntervalSince1970 * 1000))_\(UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased())"
|
|
383
|
+
let pendingStart = Date().timeIntervalSince1970
|
|
384
|
+
self.stateLock.lock()
|
|
385
|
+
self.state = .starting(sessionId: pendingSessionId, startTime: pendingStart)
|
|
386
|
+
self.stateLock.unlock()
|
|
387
|
+
|
|
379
388
|
ReplayOrchestrator.shared.beginReplay(apiToken: publicKey, serverEndpoint: apiUrl, captureSettings: config)
|
|
380
389
|
|
|
381
390
|
// Allow orchestrator time to spin up
|
|
382
391
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
383
|
-
let sid = ReplayOrchestrator.shared.replayId ??
|
|
392
|
+
let sid = ReplayOrchestrator.shared.replayId ?? pendingSessionId
|
|
384
393
|
let start = Date().timeIntervalSince1970
|
|
385
394
|
|
|
386
395
|
self.stateLock.lock()
|
|
@@ -235,10 +235,14 @@ public final class ReplayOrchestrator: NSObject {
|
|
|
235
235
|
]
|
|
236
236
|
let queueDepthAtFinalize = TelemetryPipeline.shared.getQueueDepth()
|
|
237
237
|
|
|
238
|
+
// Capture the current generation so a stale halt posted here won't
|
|
239
|
+
// stop a new session's capture that starts before this block runs.
|
|
240
|
+
let haltGeneration = VisualCapture.shared.captureGeneration
|
|
241
|
+
|
|
238
242
|
// Do local teardown immediately so lifecycle rollover never depends on network latency.
|
|
239
243
|
DispatchQueue.main.async {
|
|
240
244
|
TelemetryPipeline.shared.shutdown()
|
|
241
|
-
VisualCapture.shared.halt()
|
|
245
|
+
VisualCapture.shared.halt(expectedGeneration: haltGeneration)
|
|
242
246
|
InteractionRecorder.shared.deactivate()
|
|
243
247
|
FaultTracker.shared.deactivate()
|
|
244
248
|
ResponsivenessWatcher.shared.halt()
|
|
@@ -127,6 +127,9 @@ public final class TelemetryPipeline: NSObject {
|
|
|
127
127
|
|
|
128
128
|
SegmentDispatcher.shared.halt()
|
|
129
129
|
_appSuspending()
|
|
130
|
+
|
|
131
|
+
// Clear pending frame bundles so they don't leak into the next session
|
|
132
|
+
_frameQueue.clear()
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
@objc public func finalizeAndShip() {
|
|
@@ -146,8 +149,11 @@ public final class TelemetryPipeline: NSObject {
|
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
@objc public func submitFrameBundle(payload: Data, filename: String, startMs: UInt64, endMs: UInt64, frameCount: Int) {
|
|
152
|
+
// Capture the session ID now so frames are always attributed to the
|
|
153
|
+
// session that was active when they were captured, not when they ship.
|
|
154
|
+
let capturedSessionId = currentReplayId
|
|
149
155
|
_serialWorker.async {
|
|
150
|
-
let bundle = PendingFrameBundle(tag: filename, payload: payload, rangeStart: startMs, rangeEnd: endMs, count: frameCount)
|
|
156
|
+
let bundle = PendingFrameBundle(tag: filename, payload: payload, rangeStart: startMs, rangeEnd: endMs, count: frameCount, sessionId: capturedSessionId)
|
|
151
157
|
self._frameQueue.enqueue(bundle)
|
|
152
158
|
if !self._deferredMode { self._shipPendingFrames() }
|
|
153
159
|
}
|
|
@@ -246,7 +252,19 @@ public final class TelemetryPipeline: NSObject {
|
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
private func _shipPendingFrames() {
|
|
249
|
-
guard !_deferredMode, let next = _frameQueue.dequeue()
|
|
255
|
+
guard !_deferredMode, let next = _frameQueue.dequeue() else { return }
|
|
256
|
+
|
|
257
|
+
guard currentReplayId != nil else {
|
|
258
|
+
_frameQueue.requeue(next)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Drop frames that belong to a session that is no longer active
|
|
263
|
+
if let bundleSession = next.sessionId, bundleSession != currentReplayId {
|
|
264
|
+
DiagnosticLog.trace("[TelemetryPipeline] Dropping \(next.count) stale frames from session \(bundleSession)")
|
|
265
|
+
_shipPendingFrames()
|
|
266
|
+
return
|
|
267
|
+
}
|
|
250
268
|
|
|
251
269
|
SegmentDispatcher.shared.transmitFrameBundle(
|
|
252
270
|
payload: next.payload,
|
|
@@ -569,6 +587,7 @@ private struct PendingFrameBundle {
|
|
|
569
587
|
let rangeStart: UInt64
|
|
570
588
|
let rangeEnd: UInt64
|
|
571
589
|
let count: Int
|
|
590
|
+
let sessionId: String?
|
|
572
591
|
}
|
|
573
592
|
|
|
574
593
|
private final class FrameBundleQueue {
|
|
@@ -605,4 +624,10 @@ private final class FrameBundleQueue {
|
|
|
605
624
|
defer { _lock.unlock() }
|
|
606
625
|
_queue.insert(bundle, at: 0)
|
|
607
626
|
}
|
|
627
|
+
|
|
628
|
+
func clear() {
|
|
629
|
+
_lock.lock()
|
|
630
|
+
defer { _lock.unlock() }
|
|
631
|
+
_queue.removeAll()
|
|
632
|
+
}
|
|
608
633
|
}
|
|
@@ -44,6 +44,7 @@ public final class VisualCapture: NSObject {
|
|
|
44
44
|
private var _deferredUntilCommit = false
|
|
45
45
|
private var _framesDiskPath: URL?
|
|
46
46
|
private var _currentSessionId: String?
|
|
47
|
+
@objc public private(set) var captureGeneration: Int = 0
|
|
47
48
|
|
|
48
49
|
// Use OperationQueue like industry standard - serialized, utility QoS
|
|
49
50
|
private let _encodeQueue: OperationQueue = {
|
|
@@ -105,7 +106,28 @@ public final class VisualCapture: NSObject {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
@objc public func beginCapture(sessionOrigin: UInt64) {
|
|
109
|
+
// If still in CAPTURING state (halt() from previous session hasn't
|
|
110
|
+
// run yet), force-halt first to prevent it from stopping the new session.
|
|
111
|
+
if _stateMachine.currentState == .capturing {
|
|
112
|
+
DiagnosticLog.trace("[VisualCapture] Force-halting stale capture before starting new session")
|
|
113
|
+
_stopCaptureTimer()
|
|
114
|
+
_ = _stateMachine.transition(to: .halted)
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
guard _stateMachine.transition(to: .capturing) else { return }
|
|
118
|
+
|
|
119
|
+
// Bump generation so any stale halt() becomes a no-op
|
|
120
|
+
captureGeneration += 1
|
|
121
|
+
|
|
122
|
+
// Discard leftover frames from the previous session
|
|
123
|
+
_stateLock.lock()
|
|
124
|
+
let staleCount = _screenshots.count
|
|
125
|
+
if staleCount > 0 {
|
|
126
|
+
DiagnosticLog.trace("[VisualCapture] Clearing \(staleCount) stale frames from previous session")
|
|
127
|
+
_screenshots.removeAll()
|
|
128
|
+
}
|
|
129
|
+
_stateLock.unlock()
|
|
130
|
+
|
|
109
131
|
_sessionEpoch = sessionOrigin
|
|
110
132
|
_frameCounter = 0
|
|
111
133
|
|
|
@@ -120,7 +142,13 @@ public final class VisualCapture: NSObject {
|
|
|
120
142
|
_startCaptureTimer()
|
|
121
143
|
}
|
|
122
144
|
|
|
123
|
-
@objc public func halt() {
|
|
145
|
+
@objc public func halt(expectedGeneration: Int = -1) {
|
|
146
|
+
// If a specific generation is expected (async/posted halt from a previous
|
|
147
|
+
// session), skip if a new session has already started capture.
|
|
148
|
+
if expectedGeneration >= 0 && expectedGeneration != captureGeneration {
|
|
149
|
+
DiagnosticLog.trace("[VisualCapture] Skipping stale halt (gen=\(expectedGeneration), current=\(captureGeneration))")
|
|
150
|
+
return
|
|
151
|
+
}
|
|
124
152
|
guard _stateMachine.transition(to: .halted) else { return }
|
|
125
153
|
_stopCaptureTimer()
|
|
126
154
|
|
package/lib/commonjs/index.js
CHANGED
|
@@ -292,7 +292,7 @@ let _authErrorSubscription = null;
|
|
|
292
292
|
let _currentAppState = 'active'; // Default to active, will be updated on init
|
|
293
293
|
let _userIdentity = null;
|
|
294
294
|
let _backgroundEntryTime = null; // Track when app went to background
|
|
295
|
-
|
|
295
|
+
const _storedMetadata = {}; // Accumulate metadata for session rollover
|
|
296
296
|
|
|
297
297
|
// Session timeout - must match native side (60 seconds)
|
|
298
298
|
const SESSION_TIMEOUT_MS = 60_000;
|
package/lib/module/index.js
CHANGED
|
@@ -189,7 +189,7 @@ let _authErrorSubscription = null;
|
|
|
189
189
|
let _currentAppState = 'active'; // Default to active, will be updated on init
|
|
190
190
|
let _userIdentity = null;
|
|
191
191
|
let _backgroundEntryTime = null; // Track when app went to background
|
|
192
|
-
|
|
192
|
+
const _storedMetadata = {}; // Accumulate metadata for session rollover
|
|
193
193
|
|
|
194
194
|
// Session timeout - must match native side (60 seconds)
|
|
195
195
|
const SESSION_TIMEOUT_MS = 60_000;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -212,7 +212,7 @@ let _authErrorSubscription: { remove: () => void } | null = null;
|
|
|
212
212
|
let _currentAppState: string = 'active'; // Default to active, will be updated on init
|
|
213
213
|
let _userIdentity: string | null = null;
|
|
214
214
|
let _backgroundEntryTime: number | null = null; // Track when app went to background
|
|
215
|
-
|
|
215
|
+
const _storedMetadata: Record<string, string | number | boolean> = {}; // Accumulate metadata for session rollover
|
|
216
216
|
|
|
217
217
|
// Session timeout - must match native side (60 seconds)
|
|
218
218
|
const SESSION_TIMEOUT_MS = 60_000;
|