@gravito/spectrum 3.0.0 → 3.0.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.
- package/README.md +100 -48
- package/README.zh-TW.md +163 -0
- package/dist/index.cjs +320 -20
- package/dist/index.d.cts +343 -2
- package/dist/index.d.ts +343 -2
- package/dist/index.js +320 -20
- package/package.json +12 -10
package/dist/index.cjs
CHANGED
|
@@ -36,49 +36,116 @@ __export(index_exports, {
|
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
|
|
39
|
+
// src/SpectrumOrbit.ts
|
|
40
|
+
var import_core = require("@gravito/core");
|
|
41
|
+
|
|
39
42
|
// src/storage/MemoryStorage.ts
|
|
40
43
|
var MemoryStorage = class {
|
|
41
44
|
requests = [];
|
|
42
45
|
logs = [];
|
|
43
46
|
queries = [];
|
|
44
47
|
maxItems = 1e3;
|
|
48
|
+
/**
|
|
49
|
+
* Initializes the memory storage.
|
|
50
|
+
*
|
|
51
|
+
* As memory storage does not require external resources, this is a no-op.
|
|
52
|
+
*
|
|
53
|
+
* @returns Resolves immediately
|
|
54
|
+
*/
|
|
45
55
|
async init() {
|
|
46
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Temporarily stores a captured HTTP request in memory.
|
|
59
|
+
*
|
|
60
|
+
* @param req - The request snapshot to store
|
|
61
|
+
*/
|
|
47
62
|
async storeRequest(req) {
|
|
48
63
|
this.requests.unshift(req);
|
|
49
64
|
this.trim(this.requests);
|
|
50
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Temporarily stores a captured log entry in memory.
|
|
68
|
+
*
|
|
69
|
+
* @param log - The log entry to store
|
|
70
|
+
*/
|
|
51
71
|
async storeLog(log) {
|
|
52
72
|
this.logs.unshift(log);
|
|
53
73
|
this.trim(this.logs);
|
|
54
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Temporarily stores a captured database query in memory.
|
|
77
|
+
*
|
|
78
|
+
* @param query - The query information to store
|
|
79
|
+
*/
|
|
55
80
|
async storeQuery(query) {
|
|
56
81
|
this.queries.unshift(query);
|
|
57
82
|
this.trim(this.queries);
|
|
58
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Retrieves a list of recent HTTP requests from memory.
|
|
86
|
+
*
|
|
87
|
+
* @param limit - Maximum number of requests to return
|
|
88
|
+
* @param offset - Number of requests to skip for pagination
|
|
89
|
+
* @returns A promise resolving to an array of captured requests
|
|
90
|
+
*/
|
|
59
91
|
async getRequests(limit = 100, offset = 0) {
|
|
60
92
|
return this.requests.slice(offset, offset + limit);
|
|
61
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Finds a specific HTTP request by its unique identifier.
|
|
96
|
+
*
|
|
97
|
+
* @param id - The unique ID of the request to find
|
|
98
|
+
* @returns The found request or null if not present
|
|
99
|
+
*/
|
|
62
100
|
async getRequest(id) {
|
|
63
101
|
return this.requests.find((r) => r.id === id) || null;
|
|
64
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Retrieves a list of recent application logs from memory.
|
|
105
|
+
*
|
|
106
|
+
* @param limit - Maximum number of logs to return
|
|
107
|
+
* @param offset - Number of logs to skip for pagination
|
|
108
|
+
* @returns A promise resolving to an array of captured logs
|
|
109
|
+
*/
|
|
65
110
|
async getLogs(limit = 100, offset = 0) {
|
|
66
111
|
return this.logs.slice(offset, offset + limit);
|
|
67
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Retrieves a list of recent database queries from memory.
|
|
115
|
+
*
|
|
116
|
+
* @param limit - Maximum number of queries to return
|
|
117
|
+
* @param offset - Number of queries to skip for pagination
|
|
118
|
+
* @returns A promise resolving to an array of captured queries
|
|
119
|
+
*/
|
|
68
120
|
async getQueries(limit = 100, offset = 0) {
|
|
69
121
|
return this.queries.slice(offset, offset + limit);
|
|
70
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Wipes all captured telemetry data from memory.
|
|
125
|
+
*
|
|
126
|
+
* @returns Resolves when all collections are emptied
|
|
127
|
+
*/
|
|
71
128
|
async clear() {
|
|
72
129
|
this.requests = [];
|
|
73
130
|
this.logs = [];
|
|
74
131
|
this.queries = [];
|
|
75
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Sets a new capacity limit and prunes existing data to fit.
|
|
135
|
+
*
|
|
136
|
+
* @param maxItems - The new maximum number of items to retain per category
|
|
137
|
+
*/
|
|
76
138
|
async prune(maxItems) {
|
|
77
139
|
this.maxItems = maxItems;
|
|
78
140
|
this.trim(this.requests);
|
|
79
141
|
this.trim(this.logs);
|
|
80
142
|
this.trim(this.queries);
|
|
81
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Truncates an array to ensure it does not exceed the maximum allowed capacity.
|
|
146
|
+
*
|
|
147
|
+
* @param arr - The target array to truncate
|
|
148
|
+
*/
|
|
82
149
|
trim(arr) {
|
|
83
150
|
if (arr.length > this.maxItems) {
|
|
84
151
|
arr.splice(this.maxItems);
|
|
@@ -88,13 +155,30 @@ var MemoryStorage = class {
|
|
|
88
155
|
|
|
89
156
|
// src/SpectrumOrbit.ts
|
|
90
157
|
var SpectrumOrbit = class _SpectrumOrbit {
|
|
158
|
+
/**
|
|
159
|
+
* Global instance of SpectrumOrbit.
|
|
160
|
+
*
|
|
161
|
+
* Used for internal access and singleton patterns within the framework.
|
|
162
|
+
*/
|
|
91
163
|
static instance;
|
|
92
164
|
name = "spectrum";
|
|
93
165
|
config;
|
|
94
166
|
// Event listeners for SSE
|
|
95
167
|
listeners = /* @__PURE__ */ new Set();
|
|
168
|
+
currentRequestId = null;
|
|
96
169
|
warnedSecurity = false;
|
|
97
170
|
deps;
|
|
171
|
+
/**
|
|
172
|
+
* Initializes a new instance of SpectrumOrbit.
|
|
173
|
+
*
|
|
174
|
+
* @param config - Configuration options for behavior and storage
|
|
175
|
+
* @param deps - Internal dependencies for integration with other orbits
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* const spectrum = new SpectrumOrbit({ path: '/debug' });
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
98
182
|
constructor(config = {}, deps = {}) {
|
|
99
183
|
this.config = {
|
|
100
184
|
path: config.path || "/gravito/spectrum",
|
|
@@ -107,29 +191,60 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
107
191
|
this.deps = deps;
|
|
108
192
|
_SpectrumOrbit.instance = this;
|
|
109
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Checks if the current operation should be captured based on sample rate.
|
|
196
|
+
*
|
|
197
|
+
* @returns True if capture is allowed
|
|
198
|
+
*/
|
|
110
199
|
shouldCapture() {
|
|
111
200
|
if (this.config.sampleRate >= 1) {
|
|
112
201
|
return true;
|
|
113
202
|
}
|
|
114
203
|
return Math.random() < this.config.sampleRate;
|
|
115
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Broadcasts telemetry data to all connected SSE clients.
|
|
207
|
+
*
|
|
208
|
+
* @param type - The category of the data
|
|
209
|
+
* @param data - The telemetry payload
|
|
210
|
+
*/
|
|
116
211
|
broadcast(type, data) {
|
|
117
212
|
const payload = JSON.stringify({ type, data });
|
|
118
213
|
for (const listener of this.listeners) {
|
|
119
214
|
listener(payload);
|
|
120
215
|
}
|
|
121
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Installs the Spectrum orbit into the Gravito core.
|
|
219
|
+
*
|
|
220
|
+
* Sets up collection listeners, initializes storage, and registers API/UI routes.
|
|
221
|
+
*
|
|
222
|
+
* @param core - The planet core instance
|
|
223
|
+
* @returns Resolves when installation is complete
|
|
224
|
+
* @throws {Error} If storage initialization fails
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* await spectrum.install(core);
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
122
231
|
async install(core) {
|
|
123
232
|
if (!this.config.enabled) {
|
|
124
233
|
return;
|
|
125
234
|
}
|
|
126
235
|
await this.config.storage.init();
|
|
236
|
+
await this.config.storage.prune(this.config.maxItems);
|
|
127
237
|
this.setupHttpCollection(core);
|
|
128
238
|
this.setupLogCollection(core);
|
|
129
239
|
this.setupDatabaseCollection(core);
|
|
130
240
|
this.registerRoutes(core);
|
|
131
241
|
core.logger.info(`[Spectrum] Debug Dashboard initialized at ${this.config.path}`);
|
|
132
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Configures collection of database queries from Atlas orbit.
|
|
245
|
+
*
|
|
246
|
+
* @param core - The planet core instance
|
|
247
|
+
*/
|
|
133
248
|
setupDatabaseCollection(core) {
|
|
134
249
|
const attachListener = (atlas) => {
|
|
135
250
|
if (!atlas?.Connection) {
|
|
@@ -141,7 +256,8 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
141
256
|
}
|
|
142
257
|
const data = {
|
|
143
258
|
id: crypto.randomUUID(),
|
|
144
|
-
...query
|
|
259
|
+
...query,
|
|
260
|
+
requestId: this.currentRequestId || void 0
|
|
145
261
|
};
|
|
146
262
|
this.config.storage.storeQuery(data);
|
|
147
263
|
this.broadcast("query", data);
|
|
@@ -169,21 +285,29 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
169
285
|
} catch (_e) {
|
|
170
286
|
}
|
|
171
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Configures interception of HTTP requests and responses.
|
|
290
|
+
*
|
|
291
|
+
* @param core - The planet core instance
|
|
292
|
+
*/
|
|
172
293
|
setupHttpCollection(core) {
|
|
173
294
|
const middleware = async (c, next) => {
|
|
174
295
|
if (c.req.path.startsWith(this.config.path)) {
|
|
175
296
|
return await next();
|
|
176
297
|
}
|
|
298
|
+
const requestId = crypto.randomUUID();
|
|
299
|
+
this.currentRequestId = requestId;
|
|
177
300
|
const startTime = performance.now();
|
|
178
301
|
const startTimestamp = Date.now();
|
|
179
302
|
const res = await next();
|
|
180
303
|
const duration = performance.now() - startTime;
|
|
304
|
+
this.currentRequestId = null;
|
|
181
305
|
if (!this.shouldCapture()) {
|
|
182
306
|
return res;
|
|
183
307
|
}
|
|
184
308
|
const finalRes = res || c.res;
|
|
185
309
|
const request = {
|
|
186
|
-
id:
|
|
310
|
+
id: requestId,
|
|
187
311
|
method: c.req.method,
|
|
188
312
|
path: c.req.path,
|
|
189
313
|
url: c.req.url,
|
|
@@ -193,7 +317,8 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
193
317
|
ip: c.req.header("x-forwarded-for") || "127.0.0.1",
|
|
194
318
|
userAgent: c.req.header("user-agent"),
|
|
195
319
|
requestHeaders: Object.fromEntries(c.req.raw.headers.entries()),
|
|
196
|
-
responseHeaders: finalRes ? Object.fromEntries(finalRes.headers.entries()) : {}
|
|
320
|
+
responseHeaders: finalRes ? Object.fromEntries(finalRes.headers.entries()) : {},
|
|
321
|
+
requestId
|
|
197
322
|
};
|
|
198
323
|
this.config.storage.storeRequest(request);
|
|
199
324
|
this.broadcast("request", request);
|
|
@@ -201,6 +326,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
201
326
|
};
|
|
202
327
|
core.adapter.use("*", middleware);
|
|
203
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Configures interception of application logs.
|
|
331
|
+
*
|
|
332
|
+
* Wraps the core logger to capture all log calls.
|
|
333
|
+
*
|
|
334
|
+
* @param core - The planet core instance
|
|
335
|
+
*/
|
|
204
336
|
setupLogCollection(core) {
|
|
205
337
|
const originalLogger = core.logger;
|
|
206
338
|
const spectrumLogger = {
|
|
@@ -223,6 +355,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
223
355
|
};
|
|
224
356
|
core.logger = spectrumLogger;
|
|
225
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Processes and stores a captured log entry.
|
|
360
|
+
*
|
|
361
|
+
* @param level - Log severity level
|
|
362
|
+
* @param message - Primary message string
|
|
363
|
+
* @param args - Additional log arguments
|
|
364
|
+
*/
|
|
226
365
|
captureLog(level, message, args) {
|
|
227
366
|
if (!this.shouldCapture()) {
|
|
228
367
|
return;
|
|
@@ -232,11 +371,17 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
232
371
|
level,
|
|
233
372
|
message,
|
|
234
373
|
args,
|
|
235
|
-
timestamp: Date.now()
|
|
374
|
+
timestamp: Date.now(),
|
|
375
|
+
requestId: this.currentRequestId || void 0
|
|
236
376
|
};
|
|
237
377
|
this.config.storage.storeLog(log);
|
|
238
378
|
this.broadcast("log", log);
|
|
239
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Registers all API and UI routes for the Spectrum dashboard.
|
|
382
|
+
*
|
|
383
|
+
* @param core - The planet core instance
|
|
384
|
+
*/
|
|
240
385
|
registerRoutes(core) {
|
|
241
386
|
const router = core.router;
|
|
242
387
|
const apiPath = `${this.config.path}/api`;
|
|
@@ -280,6 +425,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
280
425
|
router.post(
|
|
281
426
|
`${apiPath}/clear`,
|
|
282
427
|
wrap(async (c) => {
|
|
428
|
+
if (c.req && typeof c.req.header === "function") {
|
|
429
|
+
const token = c.req.header("x-csrf-token") || (await c.req.parseBody())?._csrf;
|
|
430
|
+
const expectedToken = (0, import_core.getCsrfToken)(c);
|
|
431
|
+
if (expectedToken && (!token || token !== expectedToken)) {
|
|
432
|
+
return c.json({ error: "Invalid CSRF token" }, 419);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
283
435
|
await this.config.storage.clear();
|
|
284
436
|
return c.json({ success: true });
|
|
285
437
|
})
|
|
@@ -290,23 +442,37 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
290
442
|
const { readable, writable } = new TransformStream();
|
|
291
443
|
const writer = writable.getWriter();
|
|
292
444
|
const encoder = new TextEncoder();
|
|
445
|
+
let isClosed = false;
|
|
446
|
+
const cleanup = () => {
|
|
447
|
+
if (isClosed) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
isClosed = true;
|
|
451
|
+
clearInterval(heartbeat);
|
|
452
|
+
this.listeners.delete(send);
|
|
453
|
+
if (typeof writer.close === "function") {
|
|
454
|
+
writer.close().catch(() => {
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
};
|
|
293
458
|
const send = (payload) => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
`));
|
|
298
|
-
} catch (_e) {
|
|
299
|
-
this.listeners.delete(send);
|
|
459
|
+
if (isClosed) {
|
|
460
|
+
return;
|
|
300
461
|
}
|
|
462
|
+
Promise.resolve().then(() => writer.write(encoder.encode(`data: ${payload}
|
|
463
|
+
|
|
464
|
+
`))).catch(() => {
|
|
465
|
+
cleanup();
|
|
466
|
+
});
|
|
301
467
|
};
|
|
302
468
|
this.listeners.add(send);
|
|
303
469
|
const heartbeat = setInterval(() => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
} catch (_e) {
|
|
307
|
-
clearInterval(heartbeat);
|
|
308
|
-
this.listeners.delete(send);
|
|
470
|
+
if (isClosed) {
|
|
471
|
+
return;
|
|
309
472
|
}
|
|
473
|
+
Promise.resolve().then(() => writer.write(encoder.encode(": heartbeat\n\n"))).catch(() => {
|
|
474
|
+
cleanup();
|
|
475
|
+
});
|
|
310
476
|
}, 3e4);
|
|
311
477
|
return new Response(readable, {
|
|
312
478
|
headers: {
|
|
@@ -320,6 +486,13 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
320
486
|
router.post(
|
|
321
487
|
`${apiPath}/replay/:id`,
|
|
322
488
|
wrap(async (c) => {
|
|
489
|
+
if (c.req && typeof c.req.header === "function") {
|
|
490
|
+
const token = c.req.header("x-csrf-token") || (await c.req.parseBody())?._csrf;
|
|
491
|
+
const expectedToken = (0, import_core.getCsrfToken)(c);
|
|
492
|
+
if (expectedToken && (!token || token !== expectedToken)) {
|
|
493
|
+
return c.json({ error: "Invalid CSRF token" }, 419);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
323
496
|
const id = c.req.param("id");
|
|
324
497
|
if (!id) {
|
|
325
498
|
return c.json({ error: "ID required" }, 400);
|
|
@@ -332,7 +505,6 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
332
505
|
const replayReq = new Request(req.url, {
|
|
333
506
|
method: req.method,
|
|
334
507
|
headers: req.requestHeaders
|
|
335
|
-
// Body logic would be needed here (if captured)
|
|
336
508
|
});
|
|
337
509
|
const res = await core.adapter.fetch(replayReq);
|
|
338
510
|
return c.json({
|
|
@@ -483,7 +655,8 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
483
655
|
logs: [],
|
|
484
656
|
queries: [],
|
|
485
657
|
connected: false,
|
|
486
|
-
eventSource: null
|
|
658
|
+
eventSource: null,
|
|
659
|
+
csrfToken: this.getCsrfTokenFromCookie()
|
|
487
660
|
}
|
|
488
661
|
},
|
|
489
662
|
computed: {
|
|
@@ -505,6 +678,14 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
505
678
|
this.fetchData();
|
|
506
679
|
this.initRealtime();
|
|
507
680
|
},
|
|
681
|
+
unmounted() {
|
|
682
|
+
// Cleanup EventSource to prevent memory leaks
|
|
683
|
+
if (this.eventSource) {
|
|
684
|
+
this.eventSource.close();
|
|
685
|
+
this.eventSource = null;
|
|
686
|
+
this.connected = false;
|
|
687
|
+
}
|
|
688
|
+
},
|
|
508
689
|
methods: {
|
|
509
690
|
initRealtime() {
|
|
510
691
|
this.eventSource = new EventSource('${apiPath}/events');
|
|
@@ -543,13 +724,35 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
543
724
|
},
|
|
544
725
|
async clearData() {
|
|
545
726
|
if (confirm('Are you sure you want to clear all debug data?')) {
|
|
546
|
-
|
|
547
|
-
|
|
727
|
+
try {
|
|
728
|
+
const res = await fetch('${apiPath}/clear', {
|
|
729
|
+
method: 'POST',
|
|
730
|
+
headers: {
|
|
731
|
+
'X-CSRF-Token': this.csrfToken
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
if (res.status === 419) {
|
|
735
|
+
alert('CSRF token invalid. Please refresh the page.');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
this.fetchData();
|
|
739
|
+
} catch (e) {
|
|
740
|
+
alert('Failed to clear data: ' + e.message);
|
|
741
|
+
}
|
|
548
742
|
}
|
|
549
743
|
},
|
|
550
744
|
async replayRequest(id) {
|
|
551
745
|
try {
|
|
552
|
-
const res = await fetch('${apiPath}/replay/' + id, {
|
|
746
|
+
const res = await fetch('${apiPath}/replay/' + id, {
|
|
747
|
+
method: 'POST',
|
|
748
|
+
headers: {
|
|
749
|
+
'X-CSRF-Token': this.csrfToken
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
if (res.status === 419) {
|
|
753
|
+
alert('CSRF token invalid. Please refresh the page.');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
553
756
|
const data = await res.json();
|
|
554
757
|
if (data.success) {
|
|
555
758
|
alert('Replay successful! Status: ' + data.status);
|
|
@@ -588,6 +791,10 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
588
791
|
'debug': 'text-slate-500'
|
|
589
792
|
};
|
|
590
793
|
return map[l] || 'text-slate-400';
|
|
794
|
+
},
|
|
795
|
+
getCsrfTokenFromCookie() {
|
|
796
|
+
const match = document.cookie.match(/csrf_token=([^;]+)/);
|
|
797
|
+
return match ? match[1] : '';
|
|
591
798
|
}
|
|
592
799
|
}
|
|
593
800
|
}).mount('#app')
|
|
@@ -604,6 +811,16 @@ var SpectrumOrbit = class _SpectrumOrbit {
|
|
|
604
811
|
var import_node_fs = require("fs");
|
|
605
812
|
var import_node_path = require("path");
|
|
606
813
|
var FileStorage = class {
|
|
814
|
+
/**
|
|
815
|
+
* Initializes a new instance of FileStorage.
|
|
816
|
+
*
|
|
817
|
+
* @param config - Configuration including the target directory for JSONL files
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* ```typescript
|
|
821
|
+
* const storage = new FileStorage({ directory: './storage' });
|
|
822
|
+
* ```
|
|
823
|
+
*/
|
|
607
824
|
constructor(config) {
|
|
608
825
|
this.config = config;
|
|
609
826
|
this.requestsPath = (0, import_node_path.join)(config.directory, "spectrum-requests.jsonl");
|
|
@@ -621,6 +838,12 @@ var FileStorage = class {
|
|
|
621
838
|
logs: [],
|
|
622
839
|
queries: []
|
|
623
840
|
};
|
|
841
|
+
/**
|
|
842
|
+
* Initializes the storage by creating the directory and loading existing files into memory.
|
|
843
|
+
*
|
|
844
|
+
* @returns Resolves when initialization is complete
|
|
845
|
+
* @throws {Error} If directory creation fails
|
|
846
|
+
*/
|
|
624
847
|
async init() {
|
|
625
848
|
if (!(0, import_node_fs.existsSync)(this.config.directory)) {
|
|
626
849
|
(0, import_node_fs.mkdirSync)(this.config.directory, { recursive: true });
|
|
@@ -629,6 +852,12 @@ var FileStorage = class {
|
|
|
629
852
|
this.loadCache(this.logsPath, this.cache.logs);
|
|
630
853
|
this.loadCache(this.queriesPath, this.cache.queries);
|
|
631
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Loads newline-delimited JSON data from a file into a target array.
|
|
857
|
+
*
|
|
858
|
+
* @param path - The absolute path to the JSONL file
|
|
859
|
+
* @param target - The array to populate with parsed objects
|
|
860
|
+
*/
|
|
632
861
|
loadCache(path, target) {
|
|
633
862
|
if (!(0, import_node_fs.existsSync)(path)) {
|
|
634
863
|
return;
|
|
@@ -646,6 +875,13 @@ var FileStorage = class {
|
|
|
646
875
|
console.error(`[Spectrum] Failed to load cache from ${path}`, e);
|
|
647
876
|
}
|
|
648
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Internal helper to append data to both the in-memory cache and the JSONL file.
|
|
880
|
+
*
|
|
881
|
+
* @param path - File path to append to
|
|
882
|
+
* @param data - The telemetry object
|
|
883
|
+
* @param list - The cache array
|
|
884
|
+
*/
|
|
649
885
|
async append(path, data, list) {
|
|
650
886
|
list.unshift(data);
|
|
651
887
|
try {
|
|
@@ -655,27 +891,72 @@ var FileStorage = class {
|
|
|
655
891
|
console.error(`[Spectrum] Failed to write to ${path}`, e);
|
|
656
892
|
}
|
|
657
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Persists a captured HTTP request.
|
|
896
|
+
*
|
|
897
|
+
* @param req - The request snapshot
|
|
898
|
+
*/
|
|
658
899
|
async storeRequest(req) {
|
|
659
900
|
await this.append(this.requestsPath, req, this.cache.requests);
|
|
660
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* Persists a captured log entry.
|
|
904
|
+
*
|
|
905
|
+
* @param log - The log snapshot
|
|
906
|
+
*/
|
|
661
907
|
async storeLog(log) {
|
|
662
908
|
await this.append(this.logsPath, log, this.cache.logs);
|
|
663
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Persists a captured database query.
|
|
912
|
+
*
|
|
913
|
+
* @param query - The query snapshot
|
|
914
|
+
*/
|
|
664
915
|
async storeQuery(query) {
|
|
665
916
|
await this.append(this.queriesPath, query, this.cache.queries);
|
|
666
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* Retrieves recent HTTP requests from storage.
|
|
920
|
+
*
|
|
921
|
+
* @param limit - Maximum number of items to return
|
|
922
|
+
* @param offset - Pagination offset
|
|
923
|
+
* @returns Array of requests
|
|
924
|
+
*/
|
|
667
925
|
async getRequests(limit = 100, offset = 0) {
|
|
668
926
|
return this.cache.requests.slice(offset, offset + limit);
|
|
669
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* Retrieves a specific HTTP request by its unique ID.
|
|
930
|
+
*
|
|
931
|
+
* @param id - The snapshot ID
|
|
932
|
+
* @returns The request or null if not found
|
|
933
|
+
*/
|
|
670
934
|
async getRequest(id) {
|
|
671
935
|
return this.cache.requests.find((r) => r.id === id) || null;
|
|
672
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Retrieves recent logs from storage.
|
|
939
|
+
*
|
|
940
|
+
* @param limit - Maximum number of items to return
|
|
941
|
+
* @param offset - Pagination offset
|
|
942
|
+
* @returns Array of logs
|
|
943
|
+
*/
|
|
673
944
|
async getLogs(limit = 100, offset = 0) {
|
|
674
945
|
return this.cache.logs.slice(offset, offset + limit);
|
|
675
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Retrieves recent database queries from storage.
|
|
949
|
+
*
|
|
950
|
+
* @param limit - Maximum number of items to return
|
|
951
|
+
* @param offset - Pagination offset
|
|
952
|
+
* @returns Array of queries
|
|
953
|
+
*/
|
|
676
954
|
async getQueries(limit = 100, offset = 0) {
|
|
677
955
|
return this.cache.queries.slice(offset, offset + limit);
|
|
678
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* Wipes all data from both cache and files.
|
|
959
|
+
*/
|
|
679
960
|
async clear() {
|
|
680
961
|
this.cache.requests = [];
|
|
681
962
|
this.cache.logs = [];
|
|
@@ -684,12 +965,31 @@ var FileStorage = class {
|
|
|
684
965
|
(0, import_node_fs.writeFileSync)(this.logsPath, "");
|
|
685
966
|
(0, import_node_fs.writeFileSync)(this.queriesPath, "");
|
|
686
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* Truncates the storage to stay within the specified limit.
|
|
970
|
+
*
|
|
971
|
+
* @param maxItems - The maximum allowed records per category
|
|
972
|
+
*/
|
|
687
973
|
async prune(maxItems) {
|
|
688
974
|
if (this.cache.requests.length > maxItems) {
|
|
689
975
|
this.cache.requests = this.cache.requests.slice(0, maxItems);
|
|
690
976
|
this.rewrite(this.requestsPath, this.cache.requests);
|
|
691
977
|
}
|
|
978
|
+
if (this.cache.logs.length > maxItems) {
|
|
979
|
+
this.cache.logs = this.cache.logs.slice(0, maxItems);
|
|
980
|
+
this.rewrite(this.logsPath, this.cache.logs);
|
|
981
|
+
}
|
|
982
|
+
if (this.cache.queries.length > maxItems) {
|
|
983
|
+
this.cache.queries = this.cache.queries.slice(0, maxItems);
|
|
984
|
+
this.rewrite(this.queriesPath, this.cache.queries);
|
|
985
|
+
}
|
|
692
986
|
}
|
|
987
|
+
/**
|
|
988
|
+
* Overwrites the file content with the current in-memory data.
|
|
989
|
+
*
|
|
990
|
+
* @param path - File path to overwrite
|
|
991
|
+
* @param data - The array of data objects
|
|
992
|
+
*/
|
|
693
993
|
rewrite(path, data) {
|
|
694
994
|
const content = `${data.slice().reverse().map((d) => JSON.stringify(d)).join("\n")}
|
|
695
995
|
`;
|