react-native-debug-toolkit 3.0.0 → 3.1.2

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 (83) hide show
  1. package/README.md +115 -97
  2. package/README.zh-CN.md +113 -95
  3. package/lib/commonjs/core/initialize.js +5 -0
  4. package/lib/commonjs/core/initialize.js.map +1 -1
  5. package/lib/commonjs/index.js +23 -26
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +24 -58
  8. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -1
  9. package/lib/commonjs/utils/DaemonClient.js +721 -0
  10. package/lib/commonjs/utils/DaemonClient.js.map +1 -0
  11. package/lib/commonjs/utils/{sessionReport.js → deviceReport.js} +3 -3
  12. package/lib/commonjs/utils/deviceReport.js.map +1 -0
  13. package/lib/module/core/initialize.js +6 -0
  14. package/lib/module/core/initialize.js.map +1 -1
  15. package/lib/module/index.js +3 -5
  16. package/lib/module/index.js.map +1 -1
  17. package/lib/module/ui/panel/StreamingSettingsModal.js +21 -55
  18. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -1
  19. package/lib/module/utils/DaemonClient.js +703 -0
  20. package/lib/module/utils/DaemonClient.js.map +1 -0
  21. package/lib/module/utils/{sessionReport.js → deviceReport.js} +2 -2
  22. package/lib/module/utils/deviceReport.js.map +1 -0
  23. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  24. package/lib/typescript/src/index.d.ts +5 -10
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
  28. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
  29. package/lib/typescript/src/utils/{sessionReport.d.ts → deviceReport.d.ts} +4 -4
  30. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
  31. package/node/daemon/src/cli.js +9 -2
  32. package/node/daemon/src/console/console.html +914 -188
  33. package/node/daemon/src/constants.js +6 -0
  34. package/node/daemon/src/server.js +205 -123
  35. package/node/daemon/src/store.js +122 -45
  36. package/node/mcp/src/daemonClient.js +6 -6
  37. package/node/mcp/src/index.js +2 -2
  38. package/node/mcp/src/logs.js +5 -4
  39. package/node/mcp/src/tools.js +16 -16
  40. package/package.json +2 -2
  41. package/src/core/initialize.ts +8 -0
  42. package/src/index.ts +18 -10
  43. package/src/ui/panel/StreamingSettingsModal.tsx +25 -63
  44. package/src/utils/DaemonClient.ts +887 -0
  45. package/src/utils/{sessionReport.ts → deviceReport.ts} +6 -6
  46. package/lib/commonjs/utils/autoDetectDaemon.js +0 -141
  47. package/lib/commonjs/utils/autoDetectDaemon.js.map +0 -1
  48. package/lib/commonjs/utils/daemonConnection.js +0 -81
  49. package/lib/commonjs/utils/daemonConnection.js.map +0 -1
  50. package/lib/commonjs/utils/daemonSettings.js +0 -110
  51. package/lib/commonjs/utils/daemonSettings.js.map +0 -1
  52. package/lib/commonjs/utils/reportToDaemon.js +0 -112
  53. package/lib/commonjs/utils/reportToDaemon.js.map +0 -1
  54. package/lib/commonjs/utils/sessionReport.js.map +0 -1
  55. package/lib/commonjs/utils/streamToDaemon.js +0 -334
  56. package/lib/commonjs/utils/streamToDaemon.js.map +0 -1
  57. package/lib/module/utils/autoDetectDaemon.js +0 -136
  58. package/lib/module/utils/autoDetectDaemon.js.map +0 -1
  59. package/lib/module/utils/daemonConnection.js +0 -77
  60. package/lib/module/utils/daemonConnection.js.map +0 -1
  61. package/lib/module/utils/daemonSettings.js +0 -102
  62. package/lib/module/utils/daemonSettings.js.map +0 -1
  63. package/lib/module/utils/reportToDaemon.js +0 -105
  64. package/lib/module/utils/reportToDaemon.js.map +0 -1
  65. package/lib/module/utils/sessionReport.js.map +0 -1
  66. package/lib/module/utils/streamToDaemon.js +0 -328
  67. package/lib/module/utils/streamToDaemon.js.map +0 -1
  68. package/lib/typescript/src/utils/autoDetectDaemon.d.ts +0 -15
  69. package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +0 -1
  70. package/lib/typescript/src/utils/daemonConnection.d.ts +0 -18
  71. package/lib/typescript/src/utils/daemonConnection.d.ts.map +0 -1
  72. package/lib/typescript/src/utils/daemonSettings.d.ts +0 -19
  73. package/lib/typescript/src/utils/daemonSettings.d.ts.map +0 -1
  74. package/lib/typescript/src/utils/reportToDaemon.d.ts +0 -34
  75. package/lib/typescript/src/utils/reportToDaemon.d.ts.map +0 -1
  76. package/lib/typescript/src/utils/sessionReport.d.ts.map +0 -1
  77. package/lib/typescript/src/utils/streamToDaemon.d.ts +0 -23
  78. package/lib/typescript/src/utils/streamToDaemon.d.ts.map +0 -1
  79. package/src/utils/autoDetectDaemon.ts +0 -175
  80. package/src/utils/daemonConnection.ts +0 -133
  81. package/src/utils/daemonSettings.ts +0 -134
  82. package/src/utils/reportToDaemon.ts +0 -172
  83. package/src/utils/streamToDaemon.ts +0 -419
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const os = require('os');
4
+ const path = require('path');
4
5
 
5
6
  const DEFAULT_HOST = '0.0.0.0';
6
7
  const DEFAULT_PORT = 3799;
@@ -22,11 +23,16 @@ function getLanIPs() {
22
23
  return ips;
23
24
  }
24
25
 
26
+ function getDefaultDeviceStorePath() {
27
+ return path.join(os.homedir(), '.react-native-debug-toolkit', 'daemon-devices.json');
28
+ }
29
+
25
30
  module.exports = {
26
31
  DAEMON_NAME,
27
32
  DAEMON_VERSION,
28
33
  DEFAULT_HOST,
29
34
  DEFAULT_PORT,
30
35
  REPORT_PROTOCOL_VERSION,
36
+ getDefaultDeviceStorePath,
31
37
  getLanIPs,
32
38
  };
@@ -94,21 +94,54 @@ function authorizeRequest(req, res, url, token) {
94
94
  return false;
95
95
  }
96
96
 
97
- function toSessionPayload(session) {
98
- if (!session) {
97
+ function normalizeIpAddress(value) {
98
+ if (!value || typeof value !== 'string') {
99
99
  return null;
100
100
  }
101
101
 
102
+ if (value.startsWith('::ffff:')) {
103
+ return value.slice(7);
104
+ }
105
+
106
+ return value;
107
+ }
108
+
109
+ function getRequestSource(req) {
110
+ const forwarded = req.headers['x-forwarded-for'];
111
+ const forwardedIp = Array.isArray(forwarded)
112
+ ? forwarded[0]
113
+ : typeof forwarded === 'string'
114
+ ? forwarded.split(',')[0]
115
+ : null;
116
+ const ip = normalizeIpAddress((forwardedIp || '').trim()) ||
117
+ normalizeIpAddress(req.socket && req.socket.remoteAddress);
118
+ const userAgent = req.headers['user-agent'];
119
+
102
120
  return {
103
- sessionId: session.sessionId,
104
- receivedAt: session.receivedAt,
105
- logCount: session.logCount,
106
- report: session.report,
121
+ ip: ip || 'unknown',
122
+ userAgent: typeof userAgent === 'string' ? userAgent : '',
107
123
  };
108
124
  }
109
125
 
110
- function selectLogs(session, searchParams) {
111
- if (!session) {
126
+ function toDevicePayload(deviceLog) {
127
+ if (!deviceLog) {
128
+ return null;
129
+ }
130
+
131
+ return {
132
+ deviceId: deviceLog.deviceId,
133
+ firstSeenAt: deviceLog.firstSeenAt,
134
+ lastSeenAt: deviceLog.lastSeenAt,
135
+ receivedAt: deviceLog.receivedAt,
136
+ device: deviceLog.device || null,
137
+ source: deviceLog.source || null,
138
+ logCount: deviceLog.logCount,
139
+ report: deviceLog.report,
140
+ };
141
+ }
142
+
143
+ function selectLogs(deviceLog, searchParams) {
144
+ if (!deviceLog) {
112
145
  return [];
113
146
  }
114
147
 
@@ -118,7 +151,7 @@ function selectLogs(session, searchParams) {
118
151
  ? Math.min(Math.floor(limitParam), 500)
119
152
  : 50;
120
153
  const failedOnly = searchParams.get('failedOnly') === 'true';
121
- const logs = session.report.logs || {};
154
+ const logs = deviceLog.report.logs || {};
122
155
 
123
156
  let entries = [];
124
157
  if (type) {
@@ -143,12 +176,18 @@ function selectLogs(session, searchParams) {
143
176
  return entries.slice(-limit);
144
177
  }
145
178
 
146
- function broadcastSSE(clients, eventType, session, delta) {
179
+ function broadcastSSE(clients, eventType, deviceLog, delta) {
147
180
  if (clients.size === 0) return;
148
181
 
149
182
  const payload = delta
150
- ? { type: 'delta', sessionId: session.sessionId, delta, logCount: session.logCount }
151
- : { type: 'full', sessionId: session.sessionId, logCount: session.logCount, device: session.report.device || null };
183
+ ? { type: 'delta', deviceId: deviceLog.deviceId, delta, logCount: deviceLog.logCount }
184
+ : {
185
+ type: 'full',
186
+ deviceId: deviceLog.deviceId,
187
+ logCount: deviceLog.logCount,
188
+ device: deviceLog.device || null,
189
+ source: deviceLog.source || null,
190
+ };
152
191
  const data = JSON.stringify(payload);
153
192
 
154
193
  clients.forEach((client) => {
@@ -160,14 +199,136 @@ function broadcastSSE(clients, eventType, session, delta) {
160
199
  });
161
200
  }
162
201
 
202
+ // --- Route Handlers ---
203
+
204
+ function handleHealth(req, res, ctx) {
205
+ sendJson(res, 200, {
206
+ ok: true,
207
+ name: DAEMON_NAME,
208
+ version: DAEMON_VERSION,
209
+ protocolVersion: REPORT_PROTOCOL_VERSION,
210
+ ips: getLanIPs(),
211
+ deviceStore: ctx.options.deviceStorePath || null,
212
+ });
213
+ }
214
+
215
+ async function handleReport(req, res, ctx) {
216
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
217
+
218
+ try {
219
+ const report = await readJsonBody(req);
220
+ if (!isValidReport(report)) {
221
+ sendJson(res, 400, {
222
+ ok: false,
223
+ error: `Report must include version ${REPORT_PROTOCOL_VERSION} and logs object`,
224
+ });
225
+ return;
226
+ }
227
+
228
+ const deviceLog = ctx.store.saveReport(report, { source: getRequestSource(req) });
229
+ sendJson(res, 200, {
230
+ ok: true,
231
+ deviceId: deviceLog.deviceId,
232
+ receivedAt: deviceLog.receivedAt,
233
+ logCount: deviceLog.logCount,
234
+ });
235
+ } catch (error) {
236
+ sendJson(res, error.statusCode || 500, {
237
+ ok: false,
238
+ error: error.message || 'Failed to read report',
239
+ });
240
+ }
241
+ }
242
+
243
+ async function handleIngest(req, res, ctx) {
244
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
245
+
246
+ try {
247
+ const body = await readJsonBody(req);
248
+ if (!body || typeof body.deviceId !== 'string' || !body.delta) {
249
+ sendJson(res, 400, { ok: false, error: 'Must include deviceId and delta' });
250
+ return;
251
+ }
252
+
253
+ const deviceLog = ctx.store.appendLogs(body.deviceId, body.delta);
254
+ if (!deviceLog) {
255
+ sendJson(res, 404, { ok: false, error: 'Device not found' });
256
+ return;
257
+ }
258
+
259
+ sendJson(res, 200, { ok: true, deviceId: deviceLog.deviceId, logCount: deviceLog.logCount });
260
+ } catch (error) {
261
+ sendJson(res, error.statusCode || 500, {
262
+ ok: false,
263
+ error: error.message || 'Failed to ingest',
264
+ });
265
+ }
266
+ }
267
+
268
+ function handleLatestDevice(req, res, ctx) {
269
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
270
+
271
+ const deviceLog = ctx.store.getLatestDevice();
272
+ if (!deviceLog) {
273
+ sendJson(res, 404, { ok: false, error: 'No device logs have been received' });
274
+ return;
275
+ }
276
+
277
+ sendJson(res, 200, {
278
+ ok: true,
279
+ ...toDevicePayload(deviceLog),
280
+ });
281
+ }
282
+
283
+ function handleDevicesList(req, res, ctx) {
284
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
285
+
286
+ sendJson(res, 200, { ok: true, devices: ctx.store.listDevices() });
287
+ }
288
+
289
+ function handleDevicesClear(req, res, ctx) {
290
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
291
+
292
+ ctx.store.clear();
293
+ sendJson(res, 200, { ok: true });
294
+ }
295
+
296
+ function handleDeviceMatch(req, res, ctx, url, deviceMatch) {
297
+ if (!authorizeRequest(req, res, url, ctx.token)) return;
298
+
299
+ const deviceLog = ctx.store.getDevice(decodeURIComponent(deviceMatch[1]));
300
+ if (!deviceLog) {
301
+ sendJson(res, 404, { ok: false, error: 'Device not found' });
302
+ return;
303
+ }
304
+
305
+ if (url.pathname.endsWith('/logs')) {
306
+ sendJson(res, 200, {
307
+ ok: true,
308
+ deviceId: deviceLog.deviceId,
309
+ receivedAt: deviceLog.receivedAt,
310
+ logs: selectLogs(deviceLog, url.searchParams),
311
+ });
312
+ return;
313
+ }
314
+
315
+ sendJson(res, 200, {
316
+ ok: true,
317
+ ...toDevicePayload(deviceLog),
318
+ });
319
+ }
320
+
321
+ // --- Server Factory ---
322
+
163
323
  function createDaemonServer(options = {}) {
164
324
  const sseClients = new Set();
165
325
  const maxSseClients = options.maxSseClients || MAX_SSE_CLIENTS;
166
326
  let keepaliveTimer = null;
167
327
 
168
328
  const store = options.store || createMemoryStore({
169
- onUpdate(session, type, delta) {
170
- broadcastSSE(sseClients, 'logs', session, type === 'delta' ? delta : null);
329
+ storagePath: options.deviceStorePath || null,
330
+ onUpdate(deviceLog, type, delta) {
331
+ broadcastSSE(sseClients, 'logs', deviceLog, type === 'delta' ? delta : null);
171
332
  },
172
333
  });
173
334
  const token = options.token || null;
@@ -175,9 +336,12 @@ function createDaemonServer(options = {}) {
175
336
  authorize: (req, res, url) => authorizeRequest(req, res, url, token),
176
337
  });
177
338
 
339
+ const ctx = { store, token, options };
340
+
178
341
  const server = http.createServer(async (req, res) => {
179
342
  const url = new URL(req.url || '/', 'http://127.0.0.1');
180
343
  const method = req.method || 'GET';
344
+ req.url = url;
181
345
 
182
346
  if (handleConsole(req, res, url, method)) return;
183
347
 
@@ -197,9 +361,15 @@ function createDaemonServer(options = {}) {
197
361
  sseClients.add(res);
198
362
  req.on('close', () => { sseClients.delete(res); });
199
363
 
200
- const latest = store.getLatestSession();
364
+ const latest = store.getLatestDevice();
201
365
  if (latest) {
202
- const data = JSON.stringify({ type: 'full', sessionId: latest.sessionId, logCount: latest.logCount, device: latest.report.device || null });
366
+ const data = JSON.stringify({
367
+ type: 'full',
368
+ deviceId: latest.deviceId,
369
+ logCount: latest.logCount,
370
+ device: latest.device || null,
371
+ source: latest.source || null,
372
+ });
203
373
  res.write(`event: logs\ndata: ${data}\n\n`);
204
374
  }
205
375
 
@@ -219,127 +389,32 @@ function createDaemonServer(options = {}) {
219
389
  }
220
390
 
221
391
  if (method === 'GET' && url.pathname === '/health') {
222
- sendJson(res, 200, {
223
- ok: true,
224
- name: DAEMON_NAME,
225
- version: DAEMON_VERSION,
226
- protocolVersion: REPORT_PROTOCOL_VERSION,
227
- ips: getLanIPs(),
228
- });
229
- return;
392
+ return handleHealth(req, res, ctx);
230
393
  }
231
394
 
232
395
  if (method === 'POST' && url.pathname === '/report') {
233
- if (!authorizeRequest(req, res, url, token)) return;
234
-
235
- try {
236
- const report = await readJsonBody(req);
237
- if (!isValidReport(report)) {
238
- sendJson(res, 400, {
239
- ok: false,
240
- error: `Report must include version ${REPORT_PROTOCOL_VERSION} and logs object`,
241
- });
242
- return;
243
- }
244
-
245
- const session = store.saveReport(report);
246
- sendJson(res, 200, {
247
- ok: true,
248
- sessionId: session.sessionId,
249
- receivedAt: session.receivedAt,
250
- logCount: session.logCount,
251
- });
252
- } catch (error) {
253
- sendJson(res, error.statusCode || 500, {
254
- ok: false,
255
- error: error.message || 'Failed to read report',
256
- });
257
- }
258
- return;
396
+ return await handleReport(req, res, ctx);
259
397
  }
260
398
 
261
399
  if (method === 'POST' && url.pathname === '/ingest') {
262
- if (!authorizeRequest(req, res, url, token)) return;
263
-
264
- try {
265
- const body = await readJsonBody(req);
266
- if (!body || typeof body.sessionId !== 'string' || !body.delta) {
267
- sendJson(res, 400, { ok: false, error: 'Must include sessionId and delta' });
268
- return;
269
- }
270
-
271
- const session = store.appendLogs(body.sessionId, body.delta);
272
- if (!session) {
273
- sendJson(res, 404, { ok: false, error: 'Session not found' });
274
- return;
275
- }
276
-
277
- sendJson(res, 200, { ok: true, sessionId: session.sessionId, logCount: session.logCount });
278
- } catch (error) {
279
- sendJson(res, error.statusCode || 500, {
280
- ok: false,
281
- error: error.message || 'Failed to ingest',
282
- });
283
- }
284
- return;
400
+ return await handleIngest(req, res, ctx);
285
401
  }
286
402
 
287
- if (method === 'GET' && (url.pathname === '/latest' || url.pathname === '/sessions/latest')) {
288
- if (!authorizeRequest(req, res, url, token)) return;
289
-
290
- const session = store.getLatestSession();
291
- if (!session) {
292
- sendJson(res, 404, { ok: false, error: 'No debug session report has been received' });
293
- return;
294
- }
295
-
296
- sendJson(res, 200, {
297
- ok: true,
298
- ...toSessionPayload(session),
299
- });
300
- return;
403
+ if (method === 'GET' && url.pathname === '/devices/latest') {
404
+ return handleLatestDevice(req, res, ctx);
301
405
  }
302
406
 
303
- if (method === 'GET' && url.pathname === '/sessions') {
304
- if (!authorizeRequest(req, res, url, token)) return;
305
-
306
- sendJson(res, 200, { ok: true, sessions: store.listSessions() });
307
- return;
407
+ if (method === 'GET' && url.pathname === '/devices') {
408
+ return handleDevicesList(req, res, ctx);
308
409
  }
309
410
 
310
- if (method === 'DELETE' && url.pathname === '/sessions') {
311
- if (!authorizeRequest(req, res, url, token)) return;
312
-
313
- store.clear();
314
- sendJson(res, 200, { ok: true });
315
- return;
411
+ if (method === 'DELETE' && url.pathname === '/devices') {
412
+ return handleDevicesClear(req, res, ctx);
316
413
  }
317
414
 
318
- const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)(?:\/logs)?$/);
319
- if (method === 'GET' && sessionMatch) {
320
- if (!authorizeRequest(req, res, url, token)) return;
321
-
322
- const session = store.getSession(decodeURIComponent(sessionMatch[1]));
323
- if (!session) {
324
- sendJson(res, 404, { ok: false, error: 'Session not found' });
325
- return;
326
- }
327
-
328
- if (url.pathname.endsWith('/logs')) {
329
- sendJson(res, 200, {
330
- ok: true,
331
- sessionId: session.sessionId,
332
- receivedAt: session.receivedAt,
333
- logs: selectLogs(session, url.searchParams),
334
- });
335
- return;
336
- }
337
-
338
- sendJson(res, 200, {
339
- ok: true,
340
- ...toSessionPayload(session),
341
- });
342
- return;
415
+ const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)(?:\/logs)?$/);
416
+ if (method === 'GET' && deviceMatch) {
417
+ return handleDeviceMatch(req, res, ctx, url, deviceMatch);
343
418
  }
344
419
 
345
420
  sendJson(res, 404, { ok: false, error: 'Not found' });
@@ -361,5 +436,12 @@ function createDaemonServer(options = {}) {
361
436
 
362
437
  module.exports = {
363
438
  createDaemonServer,
439
+ handleHealth,
440
+ handleReport,
441
+ handleIngest,
442
+ handleLatestDevice,
443
+ handleDevicesList,
444
+ handleDevicesClear,
445
+ handleDeviceMatch,
364
446
  isValidReport,
365
447
  };
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const DEFAULT_MAX_SESSIONS = 20;
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DEFAULT_MAX_DEVICES = 20;
4
7
 
5
8
  function createLogCount(report) {
6
9
  const logs = report && typeof report === 'object' ? report.logs : undefined;
@@ -16,95 +19,169 @@ function createLogCount(report) {
16
19
  }, {});
17
20
  }
18
21
 
19
- function createSessionId(now, sequence) {
20
- return `session_${now}_${sequence}`;
22
+ function slugPart(value) {
23
+ return String(value || 'unknown')
24
+ .trim()
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9._-]+/g, '-')
27
+ .replace(/^-+|-+$/g, '')
28
+ .slice(0, 80) || 'unknown';
29
+ }
30
+
31
+ function createDeviceId(report, source) {
32
+ const device = report && typeof report === 'object' && report.device && typeof report.device === 'object'
33
+ ? report.device
34
+ : {};
35
+ return [
36
+ slugPart(device.platform),
37
+ slugPart(device.model),
38
+ slugPart(source && source.ip),
39
+ ].join('_');
40
+ }
41
+
42
+ function readPersistedDevices(storagePath, maxDevices) {
43
+ if (!storagePath) {
44
+ return [];
45
+ }
46
+
47
+ try {
48
+ const raw = fs.readFileSync(storagePath, 'utf8');
49
+ const parsed = JSON.parse(raw);
50
+ if (!Array.isArray(parsed)) {
51
+ return [];
52
+ }
53
+ return parsed
54
+ .filter((deviceLog) => (
55
+ deviceLog &&
56
+ typeof deviceLog === 'object' &&
57
+ typeof deviceLog.deviceId === 'string' &&
58
+ deviceLog.report &&
59
+ typeof deviceLog.report === 'object'
60
+ ))
61
+ .slice(-maxDevices);
62
+ } catch {
63
+ return [];
64
+ }
21
65
  }
22
66
 
23
67
  function createMemoryStore(options = {}) {
24
- const maxSessions = options.maxSessions || DEFAULT_MAX_SESSIONS;
68
+ const maxDevices = options.maxDevices || DEFAULT_MAX_DEVICES;
25
69
  const onUpdate = options.onUpdate || null;
26
- const sessions = [];
27
- let sequence = 0;
70
+ const storagePath = options.storagePath || null;
71
+ const devices = readPersistedDevices(storagePath, maxDevices);
28
72
 
29
- function saveReport(report) {
30
- const now = Date.now();
31
- sequence += 1;
73
+ function persist() {
74
+ if (!storagePath) {
75
+ return;
76
+ }
77
+
78
+ try {
79
+ fs.mkdirSync(path.dirname(storagePath), { recursive: true });
80
+ const tmpPath = `${storagePath}.tmp`;
81
+ fs.writeFileSync(tmpPath, JSON.stringify(devices, null, 2));
82
+ fs.renameSync(tmpPath, storagePath);
83
+ } catch {
84
+ // Persistence is best-effort; daemon HTTP API should keep working.
85
+ }
86
+ }
32
87
 
33
- const session = {
34
- sessionId: typeof report.sessionId === 'string' && report.sessionId
35
- ? report.sessionId
36
- : createSessionId(now, sequence),
37
- receivedAt: new Date(now).toISOString(),
88
+ function saveReport(report, metadata = {}) {
89
+ const now = Date.now();
90
+ const receivedAt = new Date(now).toISOString();
91
+ const source = metadata.source || null;
92
+ const deviceId = createDeviceId(report, source);
93
+ const existingIndex = devices.findIndex((item) => item.deviceId === deviceId);
94
+ const existing = existingIndex >= 0 ? devices[existingIndex] : null;
95
+ const deviceLog = {
96
+ deviceId,
97
+ firstSeenAt: existing ? existing.firstSeenAt : receivedAt,
98
+ lastSeenAt: receivedAt,
99
+ receivedAt,
100
+ source,
101
+ device: report.device || null,
38
102
  report,
39
103
  logCount: createLogCount(report),
40
104
  };
41
105
 
42
- const existingIndex = sessions.findIndex((item) => item.sessionId === session.sessionId);
43
106
  if (existingIndex >= 0) {
44
- sessions.splice(existingIndex, 1);
107
+ devices.splice(existingIndex, 1);
45
108
  }
46
109
 
47
- sessions.push(session);
48
- while (sessions.length > maxSessions) {
49
- sessions.shift();
110
+ devices.push(deviceLog);
111
+ while (devices.length > maxDevices) {
112
+ devices.shift();
50
113
  }
51
114
 
52
- if (onUpdate) onUpdate(session, 'full');
53
- return session;
115
+ persist();
116
+ if (onUpdate) onUpdate(deviceLog, 'full');
117
+ return deviceLog;
54
118
  }
55
119
 
56
- function appendLogs(sessionId, delta) {
57
- const session = sessions.find((s) => s.sessionId === sessionId);
58
- if (!session) return null;
120
+ function appendLogs(deviceId, delta) {
121
+ const deviceLog = devices.find((item) => item.deviceId === deviceId);
122
+ if (!deviceLog) {
123
+ return null;
124
+ }
59
125
 
60
126
  const deltaLogs = (delta && delta.logs) || {};
61
127
  Object.entries(deltaLogs).forEach(([type, entries]) => {
62
- if (!Array.isArray(entries)) return;
63
- if (!session.report.logs[type]) {
64
- session.report.logs[type] = [];
128
+ if (!Array.isArray(entries)) {
129
+ return;
130
+ }
131
+ if (!deviceLog.report.logs[type]) {
132
+ deviceLog.report.logs[type] = [];
65
133
  }
66
- session.report.logs[type].push(...entries);
134
+ deviceLog.report.logs[type].push(...entries);
67
135
  });
68
136
 
69
- session.logCount = createLogCount(session.report);
70
- if (onUpdate) onUpdate(session, 'delta', delta);
71
- return session;
137
+ deviceLog.lastSeenAt = new Date(Date.now()).toISOString();
138
+ deviceLog.receivedAt = deviceLog.lastSeenAt;
139
+ deviceLog.logCount = createLogCount(deviceLog.report);
140
+ persist();
141
+ if (onUpdate) onUpdate(deviceLog, 'delta', delta);
142
+ return deviceLog;
72
143
  }
73
144
 
74
- function listSessions() {
75
- return sessions
145
+ function listDevices() {
146
+ return devices
76
147
  .slice()
77
148
  .reverse()
78
- .map((session) => ({
79
- sessionId: session.sessionId,
80
- receivedAt: session.receivedAt,
81
- logCount: session.logCount,
149
+ .map((deviceLog) => ({
150
+ deviceId: deviceLog.deviceId,
151
+ firstSeenAt: deviceLog.firstSeenAt,
152
+ lastSeenAt: deviceLog.lastSeenAt,
153
+ receivedAt: deviceLog.receivedAt,
154
+ device: deviceLog.device || null,
155
+ source: deviceLog.source || null,
156
+ logCount: deviceLog.logCount,
82
157
  }));
83
158
  }
84
159
 
85
- function getLatestSession() {
86
- return sessions[sessions.length - 1] || null;
160
+ function getLatestDevice() {
161
+ return devices[devices.length - 1] || null;
87
162
  }
88
163
 
89
- function getSession(sessionId) {
90
- return sessions.find((session) => session.sessionId === sessionId) || null;
164
+ function getDevice(deviceId) {
165
+ return devices.find((deviceLog) => deviceLog.deviceId === deviceId) || null;
91
166
  }
92
167
 
93
168
  function clear() {
94
- sessions.splice(0, sessions.length);
169
+ devices.splice(0, devices.length);
170
+ persist();
95
171
  }
96
172
 
97
173
  return {
98
174
  appendLogs,
99
175
  clear,
100
- getLatestSession,
101
- getSession,
102
- listSessions,
176
+ getDevice,
177
+ getLatestDevice,
178
+ listDevices,
103
179
  saveReport,
104
180
  };
105
181
  }
106
182
 
107
183
  module.exports = {
184
+ createDeviceId,
108
185
  createLogCount,
109
186
  createMemoryStore,
110
187
  };
@@ -105,8 +105,8 @@ async function ensureDaemon(options = {}) {
105
105
  };
106
106
  }
107
107
 
108
- async function readSession(origin, sessionId) {
109
- const requestPath = sessionId ? `/sessions/${encodeURIComponent(sessionId)}` : '/latest';
108
+ async function readDevice(origin, deviceId) {
109
+ const requestPath = deviceId ? `/devices/${encodeURIComponent(deviceId)}` : '/devices/latest';
110
110
  const response = await requestJson(origin, requestPath, { timeoutMs: 3000 });
111
111
  if (response.status !== 200 || !response.body?.ok) {
112
112
  throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
@@ -114,8 +114,8 @@ async function readSession(origin, sessionId) {
114
114
  return response.body;
115
115
  }
116
116
 
117
- async function readSessions(origin) {
118
- const response = await requestJson(origin, '/sessions', { timeoutMs: 3000 });
117
+ async function readDevices(origin) {
118
+ const response = await requestJson(origin, '/devices', { timeoutMs: 3000 });
119
119
  if (response.status !== 200 || !response.body?.ok) {
120
120
  throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
121
121
  }
@@ -127,6 +127,6 @@ module.exports = {
127
127
  findDaemonBin,
128
128
  getDaemonOrigin,
129
129
  readHealth,
130
- readSession,
131
- readSessions,
130
+ readDevice,
131
+ readDevices,
132
132
  };