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