@gravito/spectrum 1.0.0 → 1.0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,705 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FileStorage: () => FileStorage,
34
+ MemoryStorage: () => MemoryStorage,
35
+ SpectrumOrbit: () => SpectrumOrbit
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/storage/MemoryStorage.ts
40
+ var MemoryStorage = class {
41
+ requests = [];
42
+ logs = [];
43
+ queries = [];
44
+ maxItems = 1e3;
45
+ async init() {
46
+ }
47
+ async storeRequest(req) {
48
+ this.requests.unshift(req);
49
+ this.trim(this.requests);
50
+ }
51
+ async storeLog(log) {
52
+ this.logs.unshift(log);
53
+ this.trim(this.logs);
54
+ }
55
+ async storeQuery(query) {
56
+ this.queries.unshift(query);
57
+ this.trim(this.queries);
58
+ }
59
+ async getRequests(limit = 100, offset = 0) {
60
+ return this.requests.slice(offset, offset + limit);
61
+ }
62
+ async getRequest(id) {
63
+ return this.requests.find((r) => r.id === id) || null;
64
+ }
65
+ async getLogs(limit = 100, offset = 0) {
66
+ return this.logs.slice(offset, offset + limit);
67
+ }
68
+ async getQueries(limit = 100, offset = 0) {
69
+ return this.queries.slice(offset, offset + limit);
70
+ }
71
+ async clear() {
72
+ this.requests = [];
73
+ this.logs = [];
74
+ this.queries = [];
75
+ }
76
+ async prune(maxItems) {
77
+ this.maxItems = maxItems;
78
+ this.trim(this.requests);
79
+ this.trim(this.logs);
80
+ this.trim(this.queries);
81
+ }
82
+ trim(arr) {
83
+ if (arr.length > this.maxItems) {
84
+ arr.splice(this.maxItems);
85
+ }
86
+ }
87
+ };
88
+
89
+ // src/SpectrumOrbit.ts
90
+ var SpectrumOrbit = class _SpectrumOrbit {
91
+ static instance;
92
+ name = "spectrum";
93
+ config;
94
+ // Event listeners for SSE
95
+ listeners = /* @__PURE__ */ new Set();
96
+ warnedSecurity = false;
97
+ deps;
98
+ constructor(config = {}, deps = {}) {
99
+ this.config = {
100
+ path: config.path || "/gravito/spectrum",
101
+ maxItems: config.maxItems || 100,
102
+ enabled: config.enabled !== void 0 ? config.enabled : true,
103
+ storage: config.storage || new MemoryStorage(),
104
+ gate: config.gate,
105
+ sampleRate: config.sampleRate ?? 1
106
+ };
107
+ this.deps = deps;
108
+ _SpectrumOrbit.instance = this;
109
+ }
110
+ shouldCapture() {
111
+ if (this.config.sampleRate >= 1) {
112
+ return true;
113
+ }
114
+ return Math.random() < this.config.sampleRate;
115
+ }
116
+ broadcast(type, data) {
117
+ const payload = JSON.stringify({ type, data });
118
+ for (const listener of this.listeners) {
119
+ listener(payload);
120
+ }
121
+ }
122
+ async install(core) {
123
+ if (!this.config.enabled) {
124
+ return;
125
+ }
126
+ await this.config.storage.init();
127
+ this.setupHttpCollection(core);
128
+ this.setupLogCollection(core);
129
+ this.setupDatabaseCollection(core);
130
+ this.registerRoutes(core);
131
+ core.logger.info(`[Spectrum] Debug Dashboard initialized at ${this.config.path}`);
132
+ }
133
+ setupDatabaseCollection(core) {
134
+ const attachListener = (atlas) => {
135
+ if (!atlas?.Connection) {
136
+ return;
137
+ }
138
+ atlas.Connection.queryListeners.push((query) => {
139
+ if (!this.shouldCapture()) {
140
+ return;
141
+ }
142
+ const data = {
143
+ id: crypto.randomUUID(),
144
+ ...query
145
+ };
146
+ this.config.storage.storeQuery(data);
147
+ this.broadcast("query", data);
148
+ });
149
+ core.logger.info("[Spectrum] Database query collection enabled");
150
+ };
151
+ if (this.deps.atlas) {
152
+ attachListener(this.deps.atlas);
153
+ return;
154
+ }
155
+ if (this.deps.loadAtlas) {
156
+ this.deps.loadAtlas().then((atlas) => {
157
+ if (atlas) {
158
+ attachListener(atlas);
159
+ }
160
+ }).catch((_e) => {
161
+ });
162
+ return;
163
+ }
164
+ try {
165
+ import("@gravito/atlas").then((atlas) => {
166
+ attachListener(atlas);
167
+ }).catch((_e) => {
168
+ });
169
+ } catch (_e) {
170
+ }
171
+ }
172
+ setupHttpCollection(core) {
173
+ const middleware = async (c, next) => {
174
+ if (c.req.path.startsWith(this.config.path)) {
175
+ await next();
176
+ return void 0;
177
+ }
178
+ const startTime = performance.now();
179
+ const startTimestamp = Date.now();
180
+ const res = await next();
181
+ const duration = performance.now() - startTime;
182
+ if (!this.shouldCapture()) {
183
+ return res;
184
+ }
185
+ const finalRes = res || c.res;
186
+ const request = {
187
+ id: crypto.randomUUID(),
188
+ method: c.req.method,
189
+ path: c.req.path,
190
+ url: c.req.url,
191
+ status: finalRes?.status || 404,
192
+ duration,
193
+ startTime: startTimestamp,
194
+ ip: c.req.header("x-forwarded-for") || "127.0.0.1",
195
+ userAgent: c.req.header("user-agent"),
196
+ requestHeaders: Object.fromEntries(c.req.raw.headers.entries()),
197
+ responseHeaders: finalRes ? Object.fromEntries(finalRes.headers.entries()) : {}
198
+ };
199
+ this.config.storage.storeRequest(request);
200
+ this.broadcast("request", request);
201
+ return res;
202
+ };
203
+ core.adapter.use("*", middleware);
204
+ }
205
+ setupLogCollection(core) {
206
+ const originalLogger = core.logger;
207
+ const spectrumLogger = {
208
+ debug: (msg, ...args) => {
209
+ this.captureLog("debug", msg, args);
210
+ originalLogger.debug(msg, ...args);
211
+ },
212
+ info: (msg, ...args) => {
213
+ this.captureLog("info", msg, args);
214
+ originalLogger.info(msg, ...args);
215
+ },
216
+ warn: (msg, ...args) => {
217
+ this.captureLog("warn", msg, args);
218
+ originalLogger.warn(msg, ...args);
219
+ },
220
+ error: (msg, ...args) => {
221
+ this.captureLog("error", msg, args);
222
+ originalLogger.error(msg, ...args);
223
+ }
224
+ };
225
+ core.logger = spectrumLogger;
226
+ }
227
+ captureLog(level, message, args) {
228
+ if (!this.shouldCapture()) {
229
+ return;
230
+ }
231
+ const log = {
232
+ id: crypto.randomUUID(),
233
+ level,
234
+ message,
235
+ args,
236
+ timestamp: Date.now()
237
+ };
238
+ this.config.storage.storeLog(log);
239
+ this.broadcast("log", log);
240
+ }
241
+ registerRoutes(core) {
242
+ const router = core.router;
243
+ const apiPath = `${this.config.path}/api`;
244
+ const wrap = (handler) => {
245
+ return async (c) => {
246
+ if (this.config.gate) {
247
+ const allowed = await this.config.gate(c);
248
+ if (!allowed) {
249
+ return c.json({ error: "Unauthorized" }, 403);
250
+ }
251
+ } else if (process.env.NODE_ENV === "production") {
252
+ if (!this.warnedSecurity) {
253
+ console.warn(
254
+ "[Spectrum] \u26A0\uFE0F Production access requires a security gate. Requests will be blocked."
255
+ );
256
+ this.warnedSecurity = true;
257
+ }
258
+ return c.json({ error: "Unauthorized" }, 403);
259
+ }
260
+ return handler(c);
261
+ };
262
+ };
263
+ router.get(
264
+ `${apiPath}/requests`,
265
+ wrap(async (c) => {
266
+ return c.json(await this.config.storage.getRequests());
267
+ })
268
+ );
269
+ router.get(
270
+ `${apiPath}/logs`,
271
+ wrap(async (c) => {
272
+ return c.json(await this.config.storage.getLogs());
273
+ })
274
+ );
275
+ router.get(
276
+ `${apiPath}/queries`,
277
+ wrap(async (c) => {
278
+ return c.json(await this.config.storage.getQueries());
279
+ })
280
+ );
281
+ router.post(
282
+ `${apiPath}/clear`,
283
+ wrap(async (c) => {
284
+ await this.config.storage.clear();
285
+ return c.json({ success: true });
286
+ })
287
+ );
288
+ router.get(
289
+ `${apiPath}/events`,
290
+ wrap((_c) => {
291
+ const { readable, writable } = new TransformStream();
292
+ const writer = writable.getWriter();
293
+ const encoder = new TextEncoder();
294
+ const send = (payload) => {
295
+ try {
296
+ writer.write(encoder.encode(`data: ${payload}
297
+
298
+ `));
299
+ } catch (_e) {
300
+ this.listeners.delete(send);
301
+ }
302
+ };
303
+ this.listeners.add(send);
304
+ const heartbeat = setInterval(() => {
305
+ try {
306
+ writer.write(encoder.encode(": heartbeat\n\n"));
307
+ } catch (_e) {
308
+ clearInterval(heartbeat);
309
+ this.listeners.delete(send);
310
+ }
311
+ }, 3e4);
312
+ return new Response(readable, {
313
+ headers: {
314
+ "Content-Type": "text/event-stream",
315
+ "Cache-Control": "no-cache",
316
+ Connection: "keep-alive"
317
+ }
318
+ });
319
+ })
320
+ );
321
+ router.post(
322
+ `${apiPath}/replay/:id`,
323
+ wrap(async (c) => {
324
+ const id = c.req.param("id");
325
+ if (!id) {
326
+ return c.json({ error: "ID required" }, 400);
327
+ }
328
+ const req = await this.config.storage.getRequest(id);
329
+ if (!req) {
330
+ return c.json({ error: "Request not found" }, 404);
331
+ }
332
+ try {
333
+ const replayReq = new Request(req.url, {
334
+ method: req.method,
335
+ headers: req.requestHeaders
336
+ // Body logic would be needed here (if captured)
337
+ });
338
+ const res = await core.adapter.fetch(replayReq);
339
+ return c.json({
340
+ success: true,
341
+ status: res.status,
342
+ statusText: res.statusText
343
+ });
344
+ } catch (e) {
345
+ return c.json({ success: false, error: e.message }, 500);
346
+ }
347
+ })
348
+ );
349
+ router.get(
350
+ this.config.path,
351
+ wrap((c) => {
352
+ return c.html(`
353
+ <!DOCTYPE html>
354
+ <html lang="en">
355
+ <head>
356
+ <meta charset="UTF-8">
357
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
358
+ <title>Spectrum Dashboard</title>
359
+ <script src="https://cdn.tailwindcss.com"></script>
360
+ <style>
361
+ [v-cloak] { display: none; }
362
+ </style>
363
+ </head>
364
+ <body class="bg-slate-950 text-slate-200 min-h-screen">
365
+ <div id="app" v-cloak class="p-4 md:p-8">
366
+ <header class="flex justify-between items-center mb-8 border-b border-slate-800 pb-4">
367
+ <div class="flex items-center gap-4">
368
+ <h1 class="text-2xl font-bold text-sky-400">Gravito <span class="text-white">Spectrum</span></h1>
369
+ <span class="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-400 text-[10px] font-bold uppercase tracking-wider animate-pulse" v-if="connected">Live</span>
370
+ </div>
371
+ <div class="flex gap-4">
372
+ <button @click="clearData" class="px-4 py-2 bg-rose-600 hover:bg-rose-500 rounded text-sm font-medium transition">Clear Data</button>
373
+ <button @click="fetchData" class="px-4 py-2 bg-sky-600 hover:bg-sky-500 rounded text-sm font-medium transition">Refresh</button>
374
+ </div>
375
+ </header>
376
+
377
+ <!-- Stats Bar -->
378
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
379
+ <div class="bg-slate-900 border border-slate-800 p-4 rounded-lg">
380
+ <div class="text-slate-500 text-xs uppercase font-semibold">Total Requests</div>
381
+ <div class="text-2xl font-bold text-white">{{ stats.totalRequests }}</div>
382
+ </div>
383
+ <div class="bg-slate-900 border border-slate-800 p-4 rounded-lg">
384
+ <div class="text-slate-500 text-xs uppercase font-semibold">Avg Latency</div>
385
+ <div class="text-2xl font-bold text-sky-400">{{ stats.avgLatency.toFixed(1) }}ms</div>
386
+ </div>
387
+ <div class="bg-slate-900 border border-slate-800 p-4 rounded-lg">
388
+ <div class="text-slate-500 text-xs uppercase font-semibold">Error Rate</div>
389
+ <div class="text-2xl font-bold" :class="stats.errorRate > 5 ? 'text-rose-500' : 'text-emerald-500'">{{ stats.errorRate.toFixed(1) }}%</div>
390
+ </div>
391
+ <div class="bg-slate-900 border border-slate-800 p-4 rounded-lg">
392
+ <div class="text-slate-500 text-xs uppercase font-semibold">DB Queries</div>
393
+ <div class="text-2xl font-bold text-emerald-400">{{ queries.length }}</div>
394
+ </div>
395
+ </div>
396
+
397
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
398
+ <!-- Requests Section -->
399
+ <section class="lg:col-span-2">
400
+ <h2 class="text-xl font-semibold mb-4 flex items-center gap-2">
401
+ <span class="w-3 h-3 bg-sky-500 rounded-full"></span>
402
+ Recent Requests
403
+ </h2>
404
+ <div class="overflow-x-auto bg-slate-900 rounded-lg border border-slate-800">
405
+ <table class="w-full text-left border-collapse">
406
+ <thead>
407
+ <tr class="border-b border-slate-800 text-slate-400 text-sm">
408
+ <th class="p-3">Method</th>
409
+ <th class="p-3">Path</th>
410
+ <th class="p-3">Status</th>
411
+ <th class="p-3">Duration</th>
412
+ <th class="p-3">Time</th>
413
+ </tr>
414
+ </thead>
415
+ <tbody class="text-sm">
416
+ <tr v-for="req in requests" :key="req.id" class="border-b border-slate-800/50 hover:bg-slate-800/30">
417
+ <td class="p-3 font-mono">
418
+ <span :class="getMethodClass(req.method)" class="px-2 py-0.5 rounded text-xs font-bold">{{ req.method }}</span>
419
+ </td>
420
+ <td class="p-3 truncate max-w-xs" :title="req.path">{{ req.path }}</td>
421
+ <td class="p-3">
422
+ <span :class="getStatusClass(req.status)" class="font-medium">{{ req.status }}</span>
423
+ </td>
424
+ <td class="p-3 text-slate-400">{{ req.duration.toFixed(2) }}ms</td>
425
+ <td class="p-3 text-slate-500 text-xs">
426
+ {{ formatTime(req.startTime) }}
427
+ <button @click="replayRequest(req.id)" class="ml-2 text-sky-500 hover:text-sky-400 hover:underline" title="Replay Request">\u21BA</button>
428
+ </td>
429
+ </tr>
430
+ <tr v-if="requests.length === 0">
431
+ <td colspan="5" class="p-8 text-center text-slate-500 italic">No requests captured yet.</td>
432
+ </tr>
433
+ </tbody>
434
+ </table>
435
+ </div>
436
+ </section>
437
+
438
+ <!-- Stats & Queries -->
439
+ <aside class="space-y-8">
440
+ <!-- Log Section -->
441
+ <section>
442
+ <h2 class="text-xl font-semibold mb-4 flex items-center gap-2">
443
+ <span class="w-3 h-3 bg-amber-500 rounded-full"></span>
444
+ Recent Logs
445
+ </h2>
446
+ <div class="bg-slate-900 rounded-lg border border-slate-800 p-2 max-h-[400px] overflow-y-auto font-mono text-xs">
447
+ <div v-for="log in logs" :key="log.id" class="p-2 border-b border-slate-800/50 last:border-0">
448
+ <span :class="getLogLevelClass(log.level)" class="uppercase mr-2 font-bold">{{ log.level }}</span>
449
+ <span class="text-slate-500 mr-2">[{{ formatTime(log.timestamp) }}]</span>
450
+ <span class="text-slate-300">{{ log.message }}</span>
451
+ </div>
452
+ <div v-if="logs.length === 0" class="p-4 text-center text-slate-500 italic">No logs captured yet.</div>
453
+ </div>
454
+ </section>
455
+
456
+ <!-- Query Section -->
457
+ <section>
458
+ <h2 class="text-xl font-semibold mb-4 flex items-center gap-2">
459
+ <span class="w-3 h-3 bg-emerald-500 rounded-full"></span>
460
+ Database Queries
461
+ </h2>
462
+ <div class="space-y-4 max-h-[400px] overflow-y-auto">
463
+ <div v-for="q in queries" :key="q.id" class="bg-slate-900 p-3 rounded-lg border border-slate-800 text-xs font-mono">
464
+ <div class="flex justify-between text-slate-500 mb-2">
465
+ <span>{{ q.connection }}</span>
466
+ <span>{{ q.duration.toFixed(2) }}ms</span>
467
+ </div>
468
+ <div class="text-emerald-400 break-words">{{ q.sql }}</div>
469
+ </div>
470
+ <div v-if="queries.length === 0" class="bg-slate-900 p-8 rounded-lg border border-slate-800 text-center text-slate-500 italic text-sm">No queries captured yet.</div>
471
+ </div>
472
+ </section>
473
+ </aside>
474
+ </div>
475
+ </div>
476
+
477
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
478
+ <script>
479
+ const { createApp } = Vue
480
+ createApp({
481
+ data() {
482
+ return {
483
+ requests: [],
484
+ logs: [],
485
+ queries: [],
486
+ connected: false,
487
+ eventSource: null
488
+ }
489
+ },
490
+ computed: {
491
+ stats() {
492
+ const total = this.requests.length;
493
+ if (total === 0) return { totalRequests: 0, avgLatency: 0, errorRate: 0 };
494
+
495
+ const errors = this.requests.filter(r => r.status >= 400).length;
496
+ const latencySum = this.requests.reduce((sum, r) => sum + r.duration, 0);
497
+
498
+ return {
499
+ totalRequests: total,
500
+ avgLatency: latencySum / total,
501
+ errorRate: (errors / total) * 100
502
+ };
503
+ }
504
+ },
505
+ mounted() {
506
+ this.fetchData();
507
+ this.initRealtime();
508
+ },
509
+ methods: {
510
+ initRealtime() {
511
+ this.eventSource = new EventSource('${apiPath}/events');
512
+ this.eventSource.onopen = () => this.connected = true;
513
+ this.eventSource.onerror = () => {
514
+ this.connected = false;
515
+ // EventSource automatically reconnects
516
+ };
517
+ this.eventSource.onmessage = (e) => {
518
+ const { type, data } = JSON.parse(e.data);
519
+ if (type === 'request') {
520
+ this.requests.unshift(data);
521
+ if (this.requests.length > 100) this.requests.pop();
522
+ } else if (type === 'log') {
523
+ this.logs.unshift(data);
524
+ if (this.logs.length > 100) this.logs.pop();
525
+ } else if (type === 'query') {
526
+ this.queries.unshift(data);
527
+ if (this.queries.length > 100) this.queries.pop();
528
+ }
529
+ };
530
+ },
531
+ async fetchData() {
532
+ try {
533
+ const [reqs, logs, queries] = await Promise.all([
534
+ fetch('${apiPath}/requests').then(r => r.json()),
535
+ fetch('${apiPath}/logs').then(r => r.json()),
536
+ fetch('${apiPath}/queries').then(r => r.json())
537
+ ]);
538
+ this.requests = reqs;
539
+ this.logs = logs;
540
+ this.queries = queries;
541
+ } catch (e) {
542
+ console.error('Failed to fetch spectrum data', e);
543
+ }
544
+ },
545
+ async clearData() {
546
+ if (confirm('Are you sure you want to clear all debug data?')) {
547
+ await fetch('${apiPath}/clear', { method: 'POST' });
548
+ this.fetchData();
549
+ }
550
+ },
551
+ async replayRequest(id) {
552
+ try {
553
+ const res = await fetch('${apiPath}/replay/' + id, { method: 'POST' });
554
+ const data = await res.json();
555
+ if (data.success) {
556
+ alert('Replay successful! Status: ' + data.status);
557
+ this.fetchData();
558
+ } else {
559
+ alert('Replay failed: ' + data.error);
560
+ }
561
+ } catch (e) {
562
+ alert('Replay failed: ' + e.message);
563
+ }
564
+ },
565
+ formatTime(ts) {
566
+ return new Date(ts).toLocaleTimeString();
567
+ },
568
+ getMethodClass(m) {
569
+ const map = {
570
+ 'GET': 'bg-sky-500/10 text-sky-400',
571
+ 'POST': 'bg-emerald-500/10 text-emerald-400',
572
+ 'PUT': 'bg-amber-500/10 text-amber-400',
573
+ 'DELETE': 'bg-rose-500/10 text-rose-400',
574
+ 'PATCH': 'bg-indigo-500/10 text-indigo-400'
575
+ };
576
+ return map[m] || 'bg-slate-500/10 text-slate-400';
577
+ },
578
+ getStatusClass(s) {
579
+ if (s >= 500) return 'text-rose-500';
580
+ if (s >= 400) return 'text-amber-500';
581
+ if (s >= 300) return 'text-sky-500';
582
+ return 'text-emerald-500';
583
+ },
584
+ getLogLevelClass(l) {
585
+ const map = {
586
+ 'error': 'text-rose-500',
587
+ 'warn': 'text-amber-500',
588
+ 'info': 'text-sky-500',
589
+ 'debug': 'text-slate-500'
590
+ };
591
+ return map[l] || 'text-slate-400';
592
+ }
593
+ }
594
+ }).mount('#app')
595
+ </script>
596
+ </body>
597
+ </html>
598
+ `);
599
+ })
600
+ );
601
+ }
602
+ };
603
+
604
+ // src/storage/FileStorage.ts
605
+ var import_node_fs = require("fs");
606
+ var import_node_path = require("path");
607
+ var FileStorage = class {
608
+ constructor(config) {
609
+ this.config = config;
610
+ this.requestsPath = (0, import_node_path.join)(config.directory, "spectrum-requests.jsonl");
611
+ this.logsPath = (0, import_node_path.join)(config.directory, "spectrum-logs.jsonl");
612
+ this.queriesPath = (0, import_node_path.join)(config.directory, "spectrum-queries.jsonl");
613
+ }
614
+ requestsPath;
615
+ logsPath;
616
+ queriesPath;
617
+ // In-memory cache for fast read (since we append to file)
618
+ // In a real high-throughput scenario, we might only cache the tail or read on demand.
619
+ // For simplicity and performance balance, we load on init and append to both.
620
+ cache = {
621
+ requests: [],
622
+ logs: [],
623
+ queries: []
624
+ };
625
+ async init() {
626
+ if (!(0, import_node_fs.existsSync)(this.config.directory)) {
627
+ (0, import_node_fs.mkdirSync)(this.config.directory, { recursive: true });
628
+ }
629
+ this.loadCache(this.requestsPath, this.cache.requests);
630
+ this.loadCache(this.logsPath, this.cache.logs);
631
+ this.loadCache(this.queriesPath, this.cache.queries);
632
+ }
633
+ loadCache(path, target) {
634
+ if (!(0, import_node_fs.existsSync)(path)) {
635
+ return;
636
+ }
637
+ try {
638
+ const content = (0, import_node_fs.readFileSync)(path, "utf-8");
639
+ const lines = content.split("\n").filter(Boolean);
640
+ for (const line of lines) {
641
+ try {
642
+ target.unshift(JSON.parse(line));
643
+ } catch {
644
+ }
645
+ }
646
+ } catch (e) {
647
+ console.error(`[Spectrum] Failed to load cache from ${path}`, e);
648
+ }
649
+ }
650
+ async append(path, data, list) {
651
+ list.unshift(data);
652
+ try {
653
+ (0, import_node_fs.appendFileSync)(path, `${JSON.stringify(data)}
654
+ `);
655
+ } catch (e) {
656
+ console.error(`[Spectrum] Failed to write to ${path}`, e);
657
+ }
658
+ }
659
+ async storeRequest(req) {
660
+ await this.append(this.requestsPath, req, this.cache.requests);
661
+ }
662
+ async storeLog(log) {
663
+ await this.append(this.logsPath, log, this.cache.logs);
664
+ }
665
+ async storeQuery(query) {
666
+ await this.append(this.queriesPath, query, this.cache.queries);
667
+ }
668
+ async getRequests(limit = 100, offset = 0) {
669
+ return this.cache.requests.slice(offset, offset + limit);
670
+ }
671
+ async getRequest(id) {
672
+ return this.cache.requests.find((r) => r.id === id) || null;
673
+ }
674
+ async getLogs(limit = 100, offset = 0) {
675
+ return this.cache.logs.slice(offset, offset + limit);
676
+ }
677
+ async getQueries(limit = 100, offset = 0) {
678
+ return this.cache.queries.slice(offset, offset + limit);
679
+ }
680
+ async clear() {
681
+ this.cache.requests = [];
682
+ this.cache.logs = [];
683
+ this.cache.queries = [];
684
+ (0, import_node_fs.writeFileSync)(this.requestsPath, "");
685
+ (0, import_node_fs.writeFileSync)(this.logsPath, "");
686
+ (0, import_node_fs.writeFileSync)(this.queriesPath, "");
687
+ }
688
+ async prune(maxItems) {
689
+ if (this.cache.requests.length > maxItems) {
690
+ this.cache.requests = this.cache.requests.slice(0, maxItems);
691
+ this.rewrite(this.requestsPath, this.cache.requests);
692
+ }
693
+ }
694
+ rewrite(path, data) {
695
+ const content = `${data.slice().reverse().map((d) => JSON.stringify(d)).join("\n")}
696
+ `;
697
+ (0, import_node_fs.writeFileSync)(path, content);
698
+ }
699
+ };
700
+ // Annotate the CommonJS export names for ESM import in node:
701
+ 0 && (module.exports = {
702
+ FileStorage,
703
+ MemoryStorage,
704
+ SpectrumOrbit
705
+ });