react-native-debug-toolkit 3.3.8 → 3.5.1

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.
Files changed (223) hide show
  1. package/README.md +40 -26
  2. package/README.zh-CN.md +52 -38
  3. package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitNativeLogsModule.java +146 -0
  4. package/android/src/main/java/com/reactnativedebugtoolkit/ReactNativeDebugToolkitPackage.java +5 -3
  5. package/ios/DebugToolkitNativeLogs.mm +92 -0
  6. package/lib/commonjs/constants/logLevels.js +19 -0
  7. package/lib/commonjs/constants/logLevels.js.map +1 -0
  8. package/lib/commonjs/core/initialize.js +32 -19
  9. package/lib/commonjs/core/initialize.js.map +1 -1
  10. package/lib/commonjs/features/console/ConsoleLogTab.js +4 -15
  11. package/lib/commonjs/features/console/ConsoleLogTab.js.map +1 -1
  12. package/lib/commonjs/features/console/index.js +15 -8
  13. package/lib/commonjs/features/console/index.js.map +1 -1
  14. package/lib/commonjs/features/nativeLogs/NativeLogTab.js +156 -0
  15. package/lib/commonjs/features/nativeLogs/NativeLogTab.js.map +1 -0
  16. package/lib/commonjs/features/nativeLogs/index.js +97 -0
  17. package/lib/commonjs/features/nativeLogs/index.js.map +1 -0
  18. package/lib/commonjs/features/nativeLogs/nativeLogsBridge.js +71 -0
  19. package/lib/commonjs/features/nativeLogs/nativeLogsBridge.js.map +1 -0
  20. package/lib/commonjs/features/network/NetworkLogTab.js +90 -95
  21. package/lib/commonjs/features/network/NetworkLogTab.js.map +1 -1
  22. package/lib/commonjs/features/network/index.js +7 -4
  23. package/lib/commonjs/features/network/index.js.map +1 -1
  24. package/lib/commonjs/features/sessionHistory/SessionHistoryTab.js +1046 -0
  25. package/lib/commonjs/features/sessionHistory/SessionHistoryTab.js.map +1 -0
  26. package/lib/commonjs/features/sessionHistory/index.js +104 -0
  27. package/lib/commonjs/features/sessionHistory/index.js.map +1 -0
  28. package/lib/commonjs/features/track/index.js +4 -3
  29. package/lib/commonjs/features/track/index.js.map +1 -1
  30. package/lib/commonjs/index.js +27 -0
  31. package/lib/commonjs/index.js.map +1 -1
  32. package/lib/commonjs/ui/DebugView.js +2 -0
  33. package/lib/commonjs/ui/DebugView.js.map +1 -1
  34. package/lib/commonjs/ui/panel/DebugPanel.js +67 -34
  35. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  36. package/lib/commonjs/ui/panel/FeatureIntroCard.js +151 -0
  37. package/lib/commonjs/ui/panel/FeatureIntroCard.js.map +1 -0
  38. package/lib/commonjs/ui/panel/FeatureRail.js +163 -0
  39. package/lib/commonjs/ui/panel/FeatureRail.js.map +1 -0
  40. package/lib/commonjs/ui/panel/FloatPanelView.js +119 -22
  41. package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
  42. package/lib/commonjs/ui/panel/buildFeatureSummary.js +207 -0
  43. package/lib/commonjs/ui/panel/buildFeatureSummary.js.map +1 -0
  44. package/lib/commonjs/ui/panel/filterFeatureSnapshot.js +43 -0
  45. package/lib/commonjs/ui/panel/filterFeatureSnapshot.js.map +1 -0
  46. package/lib/commonjs/ui/theme/colors.js +6 -0
  47. package/lib/commonjs/ui/theme/colors.js.map +1 -1
  48. package/lib/commonjs/utils/DaemonClient.js +30 -8
  49. package/lib/commonjs/utils/DaemonClient.js.map +1 -1
  50. package/lib/commonjs/utils/SessionManager.js +132 -0
  51. package/lib/commonjs/utils/SessionManager.js.map +1 -0
  52. package/lib/commonjs/utils/StorageAdapter.js +104 -0
  53. package/lib/commonjs/utils/StorageAdapter.js.map +1 -0
  54. package/lib/commonjs/utils/createChannelFeature.js +22 -5
  55. package/lib/commonjs/utils/createChannelFeature.js.map +1 -1
  56. package/lib/commonjs/utils/createPersistedObservableStore.js +14 -8
  57. package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
  58. package/lib/commonjs/utils/debugPreferences.js +28 -5
  59. package/lib/commonjs/utils/debugPreferences.js.map +1 -1
  60. package/lib/commonjs/utils/deviceReport.js +5 -1
  61. package/lib/commonjs/utils/deviceReport.js.map +1 -1
  62. package/lib/commonjs/utils/logRuntime.js +32 -0
  63. package/lib/commonjs/utils/logRuntime.js.map +1 -0
  64. package/lib/module/constants/logLevels.js +15 -0
  65. package/lib/module/constants/logLevels.js.map +1 -0
  66. package/lib/module/core/initialize.js +32 -19
  67. package/lib/module/core/initialize.js.map +1 -1
  68. package/lib/module/features/console/ConsoleLogTab.js +1 -12
  69. package/lib/module/features/console/ConsoleLogTab.js.map +1 -1
  70. package/lib/module/features/console/index.js +15 -8
  71. package/lib/module/features/console/index.js.map +1 -1
  72. package/lib/module/features/nativeLogs/NativeLogTab.js +151 -0
  73. package/lib/module/features/nativeLogs/NativeLogTab.js.map +1 -0
  74. package/lib/module/features/nativeLogs/index.js +91 -0
  75. package/lib/module/features/nativeLogs/index.js.map +1 -0
  76. package/lib/module/features/nativeLogs/nativeLogsBridge.js +63 -0
  77. package/lib/module/features/nativeLogs/nativeLogsBridge.js.map +1 -0
  78. package/lib/module/features/network/NetworkLogTab.js +90 -95
  79. package/lib/module/features/network/NetworkLogTab.js.map +1 -1
  80. package/lib/module/features/network/index.js +7 -4
  81. package/lib/module/features/network/index.js.map +1 -1
  82. package/lib/module/features/sessionHistory/SessionHistoryTab.js +1041 -0
  83. package/lib/module/features/sessionHistory/SessionHistoryTab.js.map +1 -0
  84. package/lib/module/features/sessionHistory/index.js +100 -0
  85. package/lib/module/features/sessionHistory/index.js.map +1 -0
  86. package/lib/module/features/track/index.js +4 -3
  87. package/lib/module/features/track/index.js.map +1 -1
  88. package/lib/module/index.js +3 -0
  89. package/lib/module/index.js.map +1 -1
  90. package/lib/module/ui/DebugView.js +2 -0
  91. package/lib/module/ui/DebugView.js.map +1 -1
  92. package/lib/module/ui/panel/DebugPanel.js +67 -34
  93. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  94. package/lib/module/ui/panel/FeatureIntroCard.js +146 -0
  95. package/lib/module/ui/panel/FeatureIntroCard.js.map +1 -0
  96. package/lib/module/ui/panel/FeatureRail.js +158 -0
  97. package/lib/module/ui/panel/FeatureRail.js.map +1 -0
  98. package/lib/module/ui/panel/FloatPanelView.js +119 -22
  99. package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
  100. package/lib/module/ui/panel/buildFeatureSummary.js +203 -0
  101. package/lib/module/ui/panel/buildFeatureSummary.js.map +1 -0
  102. package/lib/module/ui/panel/filterFeatureSnapshot.js +39 -0
  103. package/lib/module/ui/panel/filterFeatureSnapshot.js.map +1 -0
  104. package/lib/module/ui/theme/colors.js +6 -0
  105. package/lib/module/ui/theme/colors.js.map +1 -1
  106. package/lib/module/utils/DaemonClient.js +30 -8
  107. package/lib/module/utils/DaemonClient.js.map +1 -1
  108. package/lib/module/utils/SessionManager.js +127 -0
  109. package/lib/module/utils/SessionManager.js.map +1 -0
  110. package/lib/module/utils/StorageAdapter.js +96 -0
  111. package/lib/module/utils/StorageAdapter.js.map +1 -0
  112. package/lib/module/utils/createChannelFeature.js +22 -5
  113. package/lib/module/utils/createChannelFeature.js.map +1 -1
  114. package/lib/module/utils/createPersistedObservableStore.js +14 -8
  115. package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
  116. package/lib/module/utils/debugPreferences.js +27 -5
  117. package/lib/module/utils/debugPreferences.js.map +1 -1
  118. package/lib/module/utils/deviceReport.js +4 -1
  119. package/lib/module/utils/deviceReport.js.map +1 -1
  120. package/lib/module/utils/logRuntime.js +26 -0
  121. package/lib/module/utils/logRuntime.js.map +1 -0
  122. package/lib/typescript/src/constants/logLevels.d.ts +3 -0
  123. package/lib/typescript/src/constants/logLevels.d.ts.map +1 -0
  124. package/lib/typescript/src/core/initialize.d.ts +6 -0
  125. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  126. package/lib/typescript/src/features/console/ConsoleLogTab.d.ts.map +1 -1
  127. package/lib/typescript/src/features/console/index.d.ts +2 -1
  128. package/lib/typescript/src/features/console/index.d.ts.map +1 -1
  129. package/lib/typescript/src/features/nativeLogs/NativeLogTab.d.ts +4 -0
  130. package/lib/typescript/src/features/nativeLogs/NativeLogTab.d.ts.map +1 -0
  131. package/lib/typescript/src/features/nativeLogs/index.d.ts +12 -0
  132. package/lib/typescript/src/features/nativeLogs/index.d.ts.map +1 -0
  133. package/lib/typescript/src/features/nativeLogs/nativeLogsBridge.d.ts +11 -0
  134. package/lib/typescript/src/features/nativeLogs/nativeLogsBridge.d.ts.map +1 -0
  135. package/lib/typescript/src/features/network/NetworkLogTab.d.ts.map +1 -1
  136. package/lib/typescript/src/features/network/index.d.ts +2 -1
  137. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  138. package/lib/typescript/src/features/sessionHistory/SessionHistoryTab.d.ts +20 -0
  139. package/lib/typescript/src/features/sessionHistory/SessionHistoryTab.d.ts.map +1 -0
  140. package/lib/typescript/src/features/sessionHistory/index.d.ts +4 -0
  141. package/lib/typescript/src/features/sessionHistory/index.d.ts.map +1 -0
  142. package/lib/typescript/src/features/track/index.d.ts +2 -1
  143. package/lib/typescript/src/features/track/index.d.ts.map +1 -1
  144. package/lib/typescript/src/index.d.ts +7 -1
  145. package/lib/typescript/src/index.d.ts.map +1 -1
  146. package/lib/typescript/src/types/feature.d.ts +1 -1
  147. package/lib/typescript/src/types/feature.d.ts.map +1 -1
  148. package/lib/typescript/src/types/index.d.ts +3 -1
  149. package/lib/typescript/src/types/index.d.ts.map +1 -1
  150. package/lib/typescript/src/types/logs.d.ts +15 -0
  151. package/lib/typescript/src/types/logs.d.ts.map +1 -1
  152. package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
  153. package/lib/typescript/src/ui/panel/DebugPanel.d.ts +3 -1
  154. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  155. package/lib/typescript/src/ui/panel/FeatureIntroCard.d.ts +14 -0
  156. package/lib/typescript/src/ui/panel/FeatureIntroCard.d.ts.map +1 -0
  157. package/lib/typescript/src/ui/panel/FeatureRail.d.ts +16 -0
  158. package/lib/typescript/src/ui/panel/FeatureRail.d.ts.map +1 -0
  159. package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
  160. package/lib/typescript/src/ui/panel/buildFeatureSummary.d.ts +13 -0
  161. package/lib/typescript/src/ui/panel/buildFeatureSummary.d.ts.map +1 -0
  162. package/lib/typescript/src/ui/panel/filterFeatureSnapshot.d.ts +3 -0
  163. package/lib/typescript/src/ui/panel/filterFeatureSnapshot.d.ts.map +1 -0
  164. package/lib/typescript/src/ui/theme/colors.d.ts +5 -0
  165. package/lib/typescript/src/ui/theme/colors.d.ts.map +1 -1
  166. package/lib/typescript/src/utils/DaemonClient.d.ts +7 -1
  167. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -1
  168. package/lib/typescript/src/utils/SessionManager.d.ts +30 -0
  169. package/lib/typescript/src/utils/SessionManager.d.ts.map +1 -0
  170. package/lib/typescript/src/utils/StorageAdapter.d.ts +38 -0
  171. package/lib/typescript/src/utils/StorageAdapter.d.ts.map +1 -0
  172. package/lib/typescript/src/utils/createChannelFeature.d.ts +2 -0
  173. package/lib/typescript/src/utils/createChannelFeature.d.ts.map +1 -1
  174. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +4 -1
  175. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
  176. package/lib/typescript/src/utils/debugPreferences.d.ts +1 -3
  177. package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
  178. package/lib/typescript/src/utils/deviceReport.d.ts +1 -0
  179. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -1
  180. package/lib/typescript/src/utils/logRuntime.d.ts +13 -0
  181. package/lib/typescript/src/utils/logRuntime.d.ts.map +1 -0
  182. package/node/daemon/src/console/console.html +18 -0
  183. package/node/mcp/src/logs.js +1 -1
  184. package/package.json +9 -1
  185. package/src/constants/logLevels.ts +13 -0
  186. package/src/core/initialize.ts +54 -25
  187. package/src/features/console/ConsoleLogTab.tsx +1 -14
  188. package/src/features/console/index.ts +18 -8
  189. package/src/features/nativeLogs/NativeLogTab.tsx +66 -0
  190. package/src/features/nativeLogs/index.ts +94 -0
  191. package/src/features/nativeLogs/nativeLogsBridge.ts +51 -0
  192. package/src/features/network/NetworkLogTab.tsx +60 -71
  193. package/src/features/network/index.ts +12 -3
  194. package/src/features/sessionHistory/SessionHistoryTab.tsx +693 -0
  195. package/src/features/sessionHistory/index.ts +102 -0
  196. package/src/features/track/index.ts +10 -3
  197. package/src/index.ts +16 -0
  198. package/src/types/feature.ts +3 -1
  199. package/src/types/index.ts +13 -0
  200. package/src/types/logs.ts +17 -0
  201. package/src/ui/DebugView.tsx +2 -0
  202. package/src/ui/panel/DebugPanel.tsx +60 -30
  203. package/src/ui/panel/FeatureIntroCard.tsx +147 -0
  204. package/src/ui/panel/FeatureRail.tsx +165 -0
  205. package/src/ui/panel/FloatPanelView.tsx +123 -15
  206. package/src/ui/panel/buildFeatureSummary.ts +288 -0
  207. package/src/ui/panel/filterFeatureSnapshot.ts +51 -0
  208. package/src/ui/theme/colors.ts +7 -0
  209. package/src/utils/DaemonClient.ts +33 -5
  210. package/src/utils/SessionManager.ts +174 -0
  211. package/src/utils/StorageAdapter.ts +135 -0
  212. package/src/utils/createChannelFeature.ts +28 -6
  213. package/src/utils/createPersistedObservableStore.ts +18 -10
  214. package/src/utils/debugPreferences.ts +38 -7
  215. package/src/utils/deviceReport.ts +5 -1
  216. package/src/utils/logRuntime.ts +39 -0
  217. package/lib/commonjs/ui/panel/FeatureTabBar.js +0 -182
  218. package/lib/commonjs/ui/panel/FeatureTabBar.js.map +0 -1
  219. package/lib/module/ui/panel/FeatureTabBar.js +0 -177
  220. package/lib/module/ui/panel/FeatureTabBar.js.map +0 -1
  221. package/lib/typescript/src/ui/panel/FeatureTabBar.d.ts +0 -13
  222. package/lib/typescript/src/ui/panel/FeatureTabBar.d.ts.map +0 -1
  223. package/src/ui/panel/FeatureTabBar.tsx +0 -204
@@ -14,6 +14,13 @@ export const Colors = {
14
14
 
15
15
  purple: '#AF52DE',
16
16
 
17
+ // Panel-specific
18
+ railBackground: '#E8EEF6',
19
+ panelDivider: '#CED8E4',
20
+ signalRedBg: '#FFE9E7',
21
+ signalAmberBg: '#FFF1D6',
22
+ signalDefaultBg: '#E7EDF5',
23
+
17
24
  get: '#007AFF',
18
25
  post: '#34C759',
19
26
  put: '#FF9500',
@@ -56,6 +56,7 @@ export interface ReportToDaemonOptions extends DebugDeviceReportOptions {
56
56
  endpoint?: string;
57
57
  timeoutMs?: number;
58
58
  token?: string;
59
+ session?: SessionInfo;
59
60
  }
60
61
 
61
62
  export interface ReportResult {
@@ -89,6 +90,7 @@ type AbortControllerLike = { signal: unknown; abort: () => void };
89
90
  type AbortControllerCtor = new () => AbortControllerLike;
90
91
 
91
92
  type SendResult = 'ok' | 'retry' | 'auth_failed';
93
+ type SessionProvider = () => SessionInfo;
92
94
 
93
95
  // ---- Constants ----
94
96
 
@@ -186,6 +188,7 @@ export class DaemonClient {
186
188
  private _onEndpointDetected: ((url: string) => void) | undefined;
187
189
  private _restorePromise: Promise<void> | null = null;
188
190
  private _sessionId: SessionInfo | null = null;
191
+ private _sessionProvider: SessionProvider | null = null;
189
192
  private _onConnectionChange: (() => void) | undefined;
190
193
 
191
194
  constructor(options: DaemonClientOptions) {
@@ -268,9 +271,7 @@ export class DaemonClient {
268
271
  connect(options: StreamToDaemonOptions = {}): void {
269
272
  if (this._stream) return;
270
273
 
271
- if (!this._sessionId) {
272
- this._sessionId = { id: generateSessionId(), startedAt: Date.now() };
273
- }
274
+ const session = this.resolveSession();
274
275
 
275
276
  const endpoint = options.endpoint || this.resolveEndpoint();
276
277
  const reportUrl = buildDaemonUrl(endpoint, '/report');
@@ -288,7 +289,7 @@ export class DaemonClient {
288
289
  debounceMs: options.debounceMs || DEFAULT_DEBOUNCE_MS,
289
290
  timeoutMs: Math.max(0, options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
290
291
  deviceId: null,
291
- session: this._sessionId,
292
+ session,
292
293
  sending: false,
293
294
  debounceTimer: null,
294
295
  retryTimer: null,
@@ -367,6 +368,14 @@ export class DaemonClient {
367
368
  this._onConnectionChange = callback;
368
369
  }
369
370
 
371
+ setSessionProvider(provider: SessionProvider): void {
372
+ this._sessionProvider = provider;
373
+ }
374
+
375
+ clearSessionProvider(): void {
376
+ this._sessionProvider = null;
377
+ }
378
+
370
379
  // --- Restore (init-time reconnect) ---
371
380
 
372
381
  async restore(): Promise<void> {
@@ -411,7 +420,10 @@ export class DaemonClient {
411
420
  async reportOnce(options: ReportToDaemonOptions = {}): Promise<ReportResult> {
412
421
  const endpoint = options.endpoint ?? this.resolveEndpoint();
413
422
  const reportUrl = buildDaemonUrl(endpoint, '/report');
414
- const report = createDebugDeviceReport(options);
423
+ const report = createDebugDeviceReport({
424
+ ...options,
425
+ session: options.session ?? this.resolveSession(),
426
+ });
415
427
  const fetchImpl = this.resolveFetch();
416
428
 
417
429
  this.notifyEndpoint(endpoint);
@@ -475,6 +487,7 @@ export class DaemonClient {
475
487
  this._streamingEnabled = null;
476
488
  this._restorePromise = null;
477
489
  this._sessionId = null;
490
+ this._sessionProvider = null;
478
491
  }
479
492
 
480
493
  // ---- Private: Transport ----
@@ -498,6 +511,21 @@ export class DaemonClient {
498
511
  this._onEndpointDetected?.(url);
499
512
  }
500
513
 
514
+ private resolveSession(): SessionInfo {
515
+ if (this._sessionProvider) {
516
+ try {
517
+ return this._sessionProvider();
518
+ } catch {
519
+ // fall through to internal session
520
+ }
521
+ }
522
+
523
+ if (!this._sessionId) {
524
+ this._sessionId = { id: generateSessionId(), startedAt: Date.now() };
525
+ }
526
+ return this._sessionId;
527
+ }
528
+
501
529
  private emitStatus(status: StreamStatus): void {
502
530
  try {
503
531
  this._stream?.onStatus?.(status);
@@ -0,0 +1,174 @@
1
+ import type { SessionInfo } from './deviceReport';
2
+ import type { StorageAdapter } from './StorageAdapter';
3
+
4
+ export type LogFeatureKey = 'console_logs' | 'network_logs' | 'native_logs' | 'track_logs';
5
+
6
+ export type LogSession = SessionInfo;
7
+
8
+ export interface SessionIndex {
9
+ currentSessionId: string;
10
+ sessions: LogSession[];
11
+ maxSessions: number;
12
+ }
13
+
14
+ export interface SessionManagerOptions {
15
+ maxSessions?: number;
16
+ featureKeys?: LogFeatureKey[];
17
+ }
18
+
19
+ const SESSION_INDEX_KEY = '@react_native_debug_toolkit/sessions';
20
+ const DEFAULT_MAX_SESSIONS = 5;
21
+ const DEFAULT_FEATURE_KEYS: LogFeatureKey[] = ['console_logs', 'network_logs', 'native_logs', 'track_logs'];
22
+
23
+ function randomHex(): string {
24
+ return Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, '0');
25
+ }
26
+
27
+ function createSession(): LogSession {
28
+ const startedAt = Date.now();
29
+ return {
30
+ id: `${startedAt}-${randomHex()}`,
31
+ startedAt,
32
+ };
33
+ }
34
+
35
+ function isLogSession(value: unknown): value is LogSession {
36
+ return Boolean(
37
+ value &&
38
+ typeof value === 'object' &&
39
+ typeof (value as Partial<LogSession>).id === 'string' &&
40
+ typeof (value as Partial<LogSession>).startedAt === 'number',
41
+ );
42
+ }
43
+
44
+ function parseSessionIndex(raw: string | null): SessionIndex | null {
45
+ if (!raw) {
46
+ return null;
47
+ }
48
+
49
+ try {
50
+ const parsed = JSON.parse(raw) as Partial<SessionIndex>;
51
+ if (!parsed || !Array.isArray(parsed.sessions)) {
52
+ return null;
53
+ }
54
+ return {
55
+ currentSessionId: typeof parsed.currentSessionId === 'string' ? parsed.currentSessionId : '',
56
+ sessions: parsed.sessions.filter(isLogSession),
57
+ maxSessions: typeof parsed.maxSessions === 'number' ? parsed.maxSessions : DEFAULT_MAX_SESSIONS,
58
+ };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function compareNewestFirst(left: LogSession, right: LogSession): number {
65
+ return right.startedAt - left.startedAt;
66
+ }
67
+
68
+ export class SessionManager {
69
+ private readonly storage: StorageAdapter;
70
+ private readonly currentSession: LogSession;
71
+ private readonly maxSessions: number;
72
+ private readonly featureKeys: LogFeatureKey[];
73
+
74
+ constructor(storage: StorageAdapter, options: SessionManagerOptions = {}) {
75
+ this.storage = storage;
76
+ this.currentSession = createSession();
77
+ this.maxSessions = Math.max(1, Math.floor(options.maxSessions ?? DEFAULT_MAX_SESSIONS));
78
+ this.featureKeys = options.featureKeys?.length ? options.featureKeys : DEFAULT_FEATURE_KEYS;
79
+ }
80
+
81
+ async initialize(): Promise<void> {
82
+ const existing = parseSessionIndex(await this.storage.getItem(SESSION_INDEX_KEY));
83
+ const byId = new Map<string, LogSession>();
84
+
85
+ byId.set(this.currentSession.id, this.currentSession);
86
+ for (const session of existing?.sessions ?? []) {
87
+ byId.set(session.id, session);
88
+ }
89
+
90
+ const sessions = Array.from(byId.values()).sort(compareNewestFirst);
91
+ const retained = sessions.slice(0, this.maxSessions);
92
+ const removed = sessions.slice(this.maxSessions);
93
+ const index: SessionIndex = {
94
+ currentSessionId: this.currentSession.id,
95
+ sessions: retained,
96
+ maxSessions: this.maxSessions,
97
+ };
98
+
99
+ await this.storage.setItem(SESSION_INDEX_KEY, JSON.stringify(index));
100
+ await this.cleanupSessionLogs(removed);
101
+ }
102
+
103
+ getCurrentSession(): LogSession {
104
+ return this.currentSession;
105
+ }
106
+
107
+ async getSessionHistory(): Promise<LogSession[]> {
108
+ const index = parseSessionIndex(await this.storage.getItem(SESSION_INDEX_KEY));
109
+ return (index?.sessions ?? []).sort(compareNewestFirst);
110
+ }
111
+
112
+ getLogStorageKey(featureKey: LogFeatureKey, sessionId = this.currentSession.id): string {
113
+ return `@react_native_debug_toolkit/${sessionId}/${featureKey}`;
114
+ }
115
+
116
+ async loadSessionLogs<T>(sessionId: string, featureKey: LogFeatureKey): Promise<T[]> {
117
+ const raw = await this.storage.getItem(this.getLogStorageKey(featureKey, sessionId));
118
+ if (!raw) {
119
+ return [];
120
+ }
121
+
122
+ try {
123
+ const parsed = JSON.parse(raw) as unknown;
124
+ return Array.isArray(parsed) ? parsed as T[] : [];
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+
130
+ async getSessionLogCount(sessionId: string, featureKey: LogFeatureKey): Promise<number> {
131
+ const raw = await this.storage.getItem(this.getLogStorageKey(featureKey, sessionId));
132
+ if (!raw) return 0;
133
+ try {
134
+ const parsed = JSON.parse(raw);
135
+ return Array.isArray(parsed) ? parsed.length : 0;
136
+ } catch {
137
+ return 0;
138
+ }
139
+ }
140
+
141
+ async clearCurrentSessionLogs(featureKey: LogFeatureKey): Promise<void> {
142
+ await this.storage.setItem(this.getLogStorageKey(featureKey), '[]');
143
+ }
144
+
145
+ async cleanupOldSessions(): Promise<number> {
146
+ const index = parseSessionIndex(await this.storage.getItem(SESSION_INDEX_KEY));
147
+ if (!index) {
148
+ return 0;
149
+ }
150
+
151
+ const sessions = index.sessions.sort(compareNewestFirst);
152
+ const retained = sessions.slice(0, this.maxSessions);
153
+ const removed = sessions.slice(this.maxSessions);
154
+ if (removed.length === 0) {
155
+ return 0;
156
+ }
157
+
158
+ await this.storage.setItem(SESSION_INDEX_KEY, JSON.stringify({
159
+ currentSessionId: index.currentSessionId,
160
+ sessions: retained,
161
+ maxSessions: this.maxSessions,
162
+ }));
163
+ await this.cleanupSessionLogs(removed);
164
+ return removed.length;
165
+ }
166
+
167
+ private async cleanupSessionLogs(sessions: LogSession[]): Promise<void> {
168
+ for (const session of sessions) {
169
+ for (const featureKey of this.featureKeys) {
170
+ await this.storage.removeItem(this.getLogStorageKey(featureKey, session.id));
171
+ }
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,135 @@
1
+ export interface StorageAdapter {
2
+ getItem(key: string): string | null | Promise<string | null>;
3
+ setItem(key: string, value: string): void | Promise<void>;
4
+ removeItem(key: string): void | Promise<void>;
5
+ }
6
+
7
+ type AsyncStorageLike = {
8
+ getItem: (key: string) => Promise<string | null>;
9
+ setItem: (key: string, value: string) => Promise<void>;
10
+ removeItem: (key: string) => Promise<void>;
11
+ };
12
+
13
+ type MMKVLike = {
14
+ getString: (key: string) => string | undefined;
15
+ set: (key: string, value: string) => void;
16
+ delete: (key: string) => void;
17
+ };
18
+
19
+ type MMKVLikeV4 = {
20
+ getString: (key: string) => string | undefined;
21
+ set: (key: string, value: string | number | boolean | ArrayBuffer) => void;
22
+ remove: (key: string) => boolean;
23
+ };
24
+
25
+ export class MemoryStorageAdapter implements StorageAdapter {
26
+ private readonly store = new Map<string, string>();
27
+
28
+ getItem(key: string): string | null {
29
+ return this.store.get(key) ?? null;
30
+ }
31
+
32
+ setItem(key: string, value: string): void {
33
+ this.store.set(key, value);
34
+ }
35
+
36
+ removeItem(key: string): void {
37
+ this.store.delete(key);
38
+ }
39
+ }
40
+
41
+ export class AsyncStorageAdapter implements StorageAdapter {
42
+ constructor(private readonly storage: AsyncStorageLike) {}
43
+
44
+ getItem(key: string): Promise<string | null> {
45
+ return this.storage.getItem(key);
46
+ }
47
+
48
+ setItem(key: string, value: string): Promise<void> {
49
+ return this.storage.setItem(key, value);
50
+ }
51
+
52
+ removeItem(key: string): Promise<void> {
53
+ return this.storage.removeItem(key);
54
+ }
55
+ }
56
+
57
+ export class MMKVStorageAdapter implements StorageAdapter {
58
+ private readonly storage: MMKVLike;
59
+
60
+ constructor(storage: MMKVLike) {
61
+ this.storage = storage;
62
+ }
63
+
64
+ getItem(key: string): string | null {
65
+ return this.storage.getString(key) ?? null;
66
+ }
67
+
68
+ setItem(key: string, value: string): void {
69
+ this.storage.set(key, value);
70
+ }
71
+
72
+ removeItem(key: string): void {
73
+ if ('delete' in this.storage) {
74
+ (this.storage as MMKVLike).delete(key);
75
+ } else {
76
+ (this.storage as MMKVLikeV4).remove(key);
77
+ }
78
+ }
79
+ }
80
+
81
+ function isAsyncStorageLike(value: unknown): value is AsyncStorageLike {
82
+ return Boolean(
83
+ value &&
84
+ typeof value === 'object' &&
85
+ typeof (value as Partial<AsyncStorageLike>).getItem === 'function' &&
86
+ typeof (value as Partial<AsyncStorageLike>).setItem === 'function' &&
87
+ typeof (value as Partial<AsyncStorageLike>).removeItem === 'function',
88
+ );
89
+ }
90
+
91
+ function loadMMKVStorage(): StorageAdapter | null {
92
+ try {
93
+ const mod = require('react-native-mmkv');
94
+ console.warn('[StorageAdapter] MMKV module loaded, keys:', Object.keys(mod).join(','));
95
+
96
+ // v4+: createMMKV factory function
97
+ if (typeof mod?.createMMKV === 'function') {
98
+ console.warn('[StorageAdapter] Found createMMKV (v4+)');
99
+ const instance = mod.createMMKV({ id: 'debug-toolkit-logs' });
100
+ console.warn('[StorageAdapter] createMMKV returned:', typeof instance, instance ? 'has getString:' + typeof instance.getString : 'null');
101
+ if (instance && typeof instance.getString === 'function' && typeof instance.set === 'function') {
102
+ return new MMKVStorageAdapter(instance as unknown as MMKVLike);
103
+ }
104
+ }
105
+
106
+ // v3 and earlier: MMKV class
107
+ const MMKV = mod?.MMKV ?? mod?.default?.MMKV ?? mod?.default;
108
+ if (typeof MMKV === 'function') {
109
+ return new MMKVStorageAdapter(new MMKV({ id: 'debug-toolkit-logs' }));
110
+ }
111
+
112
+ console.warn('[StorageAdapter] No MMKV constructor found');
113
+ return null;
114
+ } catch (e) {
115
+ console.warn('[StorageAdapter] loadMMKVStorage failed:', e);
116
+ return null;
117
+ }
118
+ }
119
+
120
+ function loadAsyncStorage(): StorageAdapter | null {
121
+ try {
122
+ const mod = require('@react-native-async-storage/async-storage');
123
+ const storage = isAsyncStorageLike(mod?.default) ? mod.default : mod;
124
+ if (!isAsyncStorageLike(storage)) {
125
+ return null;
126
+ }
127
+ return new AsyncStorageAdapter(storage);
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ export function createDefaultLogStorage(): StorageAdapter {
134
+ return loadMMKVStorage() ?? loadAsyncStorage() ?? new MemoryStorageAdapter();
135
+ }
@@ -2,11 +2,16 @@ import type { ComponentType } from 'react';
2
2
  import type { DebugFeature, DebugFeatureListener, DebugFeatureRenderProps } from '../types';
3
3
  import type { EventChannel } from './createEventChannel';
4
4
  import { createObservableStore, type ObservableStore } from './createObservableStore';
5
- import { createPersistedObservableStore } from './createPersistedObservableStore';
5
+ import {
6
+ createPersistedObservableStore,
7
+ type PersistedObservableStore,
8
+ } from './createPersistedObservableStore';
9
+ import type { StorageAdapter } from './StorageAdapter';
6
10
 
7
11
  const DEFAULT_MAX_LOGS = 200;
8
12
 
9
13
  export interface ChannelFeaturePersistConfig<TEntry> {
14
+ storage: StorageAdapter;
10
15
  storageKey: string;
11
16
  maxPersist: number;
12
17
  debounceMs?: number;
@@ -39,15 +44,18 @@ export function createChannelFeature<TPayload, TEntry extends { id?: string }>(
39
44
  const maxLogs = options.maxLogs ?? DEFAULT_MAX_LOGS;
40
45
  let nextId = 0;
41
46
  let logStore: ObservableStore<TEntry>;
47
+ let persistedStore: PersistedObservableStore<TEntry> | null = null;
42
48
  let getId: () => string;
43
49
 
44
50
  if (options.persist) {
45
51
  const persisted = createPersistedObservableStore<TEntry>({
52
+ storage: options.persist.storage,
46
53
  storageKey: options.persist.storageKey,
47
54
  maxPersist: options.persist.maxPersist,
48
55
  debounceMs: options.persist.debounceMs,
49
56
  serialize: options.persist.serialize,
50
57
  });
58
+ persistedStore = persisted;
51
59
  logStore = persisted;
52
60
  getId = () => persisted.nextId();
53
61
  } else {
@@ -64,26 +72,40 @@ export function createChannelFeature<TPayload, TEntry extends { id?: string }>(
64
72
  label: options.label,
65
73
  renderContent: options.renderContent,
66
74
  setup: () => {
67
- if (initialized) return;
75
+ if (initialized) {
76
+ return;
77
+ }
68
78
  unsubscribe = getChannel().subscribe((payload) => {
69
79
  const filtered = options.beforePush ? options.beforePush(payload) : payload;
70
- if (filtered == null) return;
80
+ if (filtered == null) {
81
+ return;
82
+ }
71
83
  logStore.push(toEntry(filtered, getId()), maxLogs);
72
84
  });
73
85
  const cleanup = options.onSetup?.();
74
- if (cleanup) customCleanup = cleanup;
86
+ if (cleanup) {
87
+ customCleanup = cleanup;
88
+ }
75
89
  initialized = true;
76
90
  },
77
91
  getSnapshot: () => logStore.getData(),
78
92
  clear: () => {
79
- logStore.clear();
93
+ if (persistedStore) {
94
+ persistedStore.clearPersisted();
95
+ } else {
96
+ logStore.clear();
97
+ }
80
98
  },
81
99
  cleanup: () => {
82
100
  customCleanup?.();
83
101
  customCleanup = null;
84
102
  unsubscribe?.();
85
103
  unsubscribe = null;
86
- logStore.clear();
104
+ if (persistedStore) {
105
+ persistedStore.dispose();
106
+ } else {
107
+ logStore.clear();
108
+ }
87
109
  initialized = false;
88
110
  },
89
111
  subscribe: (listener: DebugFeatureListener) => logStore.subscribe(listener),
@@ -1,7 +1,8 @@
1
1
  import { createObservableStore, type ObservableStore } from './createObservableStore';
2
- import { getPreference, setPreference } from './debugPreferences';
2
+ import type { StorageAdapter } from './StorageAdapter';
3
3
 
4
4
  export interface PersistedStoreOptions<T> {
5
+ storage: StorageAdapter;
5
6
  storageKey: string;
6
7
  maxPersist: number;
7
8
  debounceMs?: number;
@@ -11,13 +12,14 @@ export interface PersistedStoreOptions<T> {
11
12
  export interface PersistedObservableStore<T> extends ObservableStore<T> {
12
13
  nextId: () => string;
13
14
  ready: Promise<void>;
14
- destroy: () => void;
15
+ clearPersisted: () => void;
16
+ dispose: () => void;
15
17
  }
16
18
 
17
19
  export function createPersistedObservableStore<T extends { id?: string }>(
18
20
  options: PersistedStoreOptions<T>,
19
21
  ): PersistedObservableStore<T> {
20
- const { storageKey, maxPersist, debounceMs = 2000, serialize } = options;
22
+ const { storage, storageKey, maxPersist, debounceMs = 2000, serialize } = options;
21
23
  const store = createObservableStore<T>();
22
24
  let writeTimer: ReturnType<typeof setTimeout> | null = null;
23
25
  let idCounter = 0;
@@ -25,7 +27,7 @@ export function createPersistedObservableStore<T extends { id?: string }>(
25
27
  const ready = new Promise<void>((resolve) => { resolveReady = resolve; });
26
28
 
27
29
  // Restore from storage (single notify via pushBatch)
28
- getPreference(storageKey).then((raw) => {
30
+ Promise.resolve(storage.getItem(storageKey)).then((raw) => {
29
31
  if (!raw) { resolveReady(); return; }
30
32
  try {
31
33
  const entries = JSON.parse(raw) as T[];
@@ -36,7 +38,9 @@ export function createPersistedObservableStore<T extends { id?: string }>(
36
38
  let max = 0;
37
39
  for (const e of restored) {
38
40
  const n = parseInt(e.id ?? '', 10);
39
- if (!isNaN(n) && n >= max) max = n + 1;
41
+ if (!isNaN(n) && n >= max) {
42
+ max = n + 1;
43
+ }
40
44
  }
41
45
  idCounter = max;
42
46
  } catch {
@@ -46,13 +50,15 @@ export function createPersistedObservableStore<T extends { id?: string }>(
46
50
  });
47
51
 
48
52
  function scheduleWrite(): void {
49
- if (writeTimer !== null) clearTimeout(writeTimer);
53
+ if (writeTimer !== null) {
54
+ clearTimeout(writeTimer);
55
+ }
50
56
  writeTimer = setTimeout(() => {
51
57
  writeTimer = null;
52
58
  const data = store.getData().slice(-maxPersist);
53
59
  const toStore = serialize ? data.map(serialize) : data;
54
60
  try {
55
- setPreference(storageKey, JSON.stringify(toStore));
61
+ Promise.resolve(storage.setItem(storageKey, JSON.stringify(toStore))).catch(() => {});
56
62
  } catch {
57
63
  // stringify failed (circular refs, etc) — skip write
58
64
  }
@@ -68,22 +74,24 @@ export function createPersistedObservableStore<T extends { id?: string }>(
68
74
  pushBatch: store.pushBatch,
69
75
  clear: () => {
70
76
  store.clear();
77
+ },
78
+ clearPersisted: () => {
79
+ store.clear();
71
80
  if (writeTimer !== null) {
72
81
  clearTimeout(writeTimer);
73
82
  writeTimer = null;
74
83
  }
75
- setPreference(storageKey, '[]');
84
+ Promise.resolve(storage.setItem(storageKey, '[]')).catch(() => {});
76
85
  },
77
86
  subscribe: store.subscribe,
78
87
  nextId: () => String(idCounter++),
79
88
  ready,
80
- destroy: () => {
89
+ dispose: () => {
81
90
  if (writeTimer !== null) {
82
91
  clearTimeout(writeTimer);
83
92
  writeTimer = null;
84
93
  }
85
94
  store.clear();
86
- setPreference(storageKey, '[]');
87
95
  },
88
96
  };
89
97
  }
@@ -1,13 +1,21 @@
1
- type AsyncStorageLike = { getItem: (k: string) => Promise<string | null>; setItem: (k: string, v: string) => Promise<void> };
2
- type NativePreferencesLike = { getPreference: (k: string) => Promise<string | null>; setPreference: (k: string, v: string) => Promise<void> };
1
+ type AsyncStorageLike = {
2
+ getItem: (k: string) => Promise<string | null>;
3
+ setItem: (k: string, v: string) => Promise<void>;
4
+ removeItem?: (k: string) => Promise<void>;
5
+ };
6
+ type NativePreferencesLike = {
7
+ getPreference: (k: string) => Promise<string | null>;
8
+ setPreference: (k: string, v: string) => Promise<void>;
9
+ };
3
10
 
4
11
  const memoryStore = new Map<string, string>();
5
12
 
6
13
  function loadAsyncStorage(): AsyncStorageLike | null {
7
14
  try {
8
15
  const mod = require('@react-native-async-storage/async-storage');
9
- if (mod && typeof mod.getItem === 'function') {
10
- return mod;
16
+ const storage = mod?.default && typeof mod.default.getItem === 'function' ? mod.default : mod;
17
+ if (storage && typeof storage.getItem === 'function' && typeof storage.setItem === 'function') {
18
+ return storage;
11
19
  }
12
20
  return null;
13
21
  } catch {
@@ -82,12 +90,35 @@ export async function getPreference(key: string): Promise<string | null> {
82
90
  return memoryStore.get(key) ?? null;
83
91
  }
84
92
 
93
+ export async function removePreference(key: string): Promise<void> {
94
+ memoryStore.delete(key);
95
+ const AsyncStorage = loadAsyncStorage();
96
+ if (AsyncStorage) {
97
+ try {
98
+ if (typeof AsyncStorage.removeItem === 'function') {
99
+ await AsyncStorage.removeItem(key);
100
+ } else {
101
+ await AsyncStorage.setItem(key, '');
102
+ }
103
+ return;
104
+ } catch {
105
+ // degrade to memory only
106
+ }
107
+ }
108
+
109
+ const nativePreferences = loadNativePreferences();
110
+ if (nativePreferences) {
111
+ try {
112
+ await nativePreferences.setPreference(key, '');
113
+ } catch {
114
+ // degrade to memory only
115
+ }
116
+ }
117
+ }
118
+
85
119
  export const KEYS = {
86
120
  fabPosition: '@react_native_debug_toolkit/fab_position',
87
121
  lastTab: '@react_native_debug_toolkit/last_tab',
88
- consoleLogs: '@react_native_debug_toolkit/console_logs',
89
- networkLogs: '@react_native_debug_toolkit/network_logs',
90
- trackLogs: '@react_native_debug_toolkit/track_logs',
91
122
  computerHost: '@react_native_debug_toolkit/computer_host',
92
123
  daemonPort: '@react_native_debug_toolkit/daemon_port',
93
124
  } as const;
@@ -33,6 +33,10 @@ export interface DebugDeviceReport {
33
33
  logs: Record<string, unknown[] | undefined>;
34
34
  }
35
35
 
36
+ export function sanitizeDebugLogEntry(value: unknown, maxBodyBytes = DEFAULT_MAX_BODY_BYTES): unknown {
37
+ return sanitizeValue(value, Math.max(256, Math.floor(maxBodyBytes)));
38
+ }
39
+
36
40
  interface TruncatedValue {
37
41
  __debugToolkitTruncated: true;
38
42
  originalBytes: number;
@@ -193,7 +197,7 @@ export function createDebugDeviceReport(
193
197
 
194
198
  logs[feature.name] = snapshot
195
199
  .slice(-maxPerType)
196
- .map((entry) => sanitizeValue(entry, maxBodyBytes));
200
+ .map((entry) => sanitizeDebugLogEntry(entry, maxBodyBytes));
197
201
  });
198
202
 
199
203
  const constants = Platform.constants as Record<string, unknown> | undefined;