234 lines
6.3 KiB
TypeScript
234 lines
6.3 KiB
TypeScript
import { logger } from './logging.js';
|
|
import * as interfaces from '../dist_ts_interfaces/index.js';
|
|
|
|
/**
|
|
* Store for TypedRequest traffic logs
|
|
* Keeps recent request/response logs in memory for dashboard display
|
|
*/
|
|
export class RequestLogStore {
|
|
private logs: interfaces.serviceworker.ITypedRequestLogEntry[] = [];
|
|
private maxEntries: number;
|
|
|
|
// Statistics
|
|
private stats: interfaces.serviceworker.ITypedRequestStats = {
|
|
totalRequests: 0,
|
|
totalResponses: 0,
|
|
methodCounts: {},
|
|
errorCount: 0,
|
|
avgDurationMs: 0,
|
|
};
|
|
|
|
// For calculating rolling average
|
|
private totalDuration = 0;
|
|
private durationCount = 0;
|
|
|
|
constructor(maxEntries = 500) {
|
|
this.maxEntries = maxEntries;
|
|
logger.log('info', `RequestLogStore initialized with max ${maxEntries} entries`);
|
|
}
|
|
|
|
/**
|
|
* Add a new log entry
|
|
* Rejects entries for serviceworker_* methods to prevent pollution from SW internal messages
|
|
*/
|
|
public addEntry(entry: interfaces.serviceworker.ITypedRequestLogEntry): void {
|
|
// Reject serviceworker_* methods - these are internal SW messages, not app traffic
|
|
// This prevents infinite loop pollution if hooks bypass somehow
|
|
if (entry.method && entry.method.startsWith('serviceworker_')) {
|
|
logger.log('note', `Rejecting serviceworker_* entry: ${entry.method}`);
|
|
return;
|
|
}
|
|
|
|
// Also reject entries with deeply nested payloads (sign of previous loop corruption)
|
|
if (this.hasNestedServiceworkerPayload(entry)) {
|
|
logger.log('warn', `Rejecting corrupted entry with nested serviceworker_* payload`);
|
|
return;
|
|
}
|
|
|
|
// Add to log
|
|
this.logs.push(entry);
|
|
|
|
// Trim if over max
|
|
if (this.logs.length > this.maxEntries) {
|
|
this.logs.shift();
|
|
}
|
|
|
|
// Update statistics
|
|
this.updateStats(entry);
|
|
}
|
|
|
|
/**
|
|
* Check if an entry has nested serviceworker_* methods in its payload (corruption from old loops)
|
|
*/
|
|
private hasNestedServiceworkerPayload(entry: interfaces.serviceworker.ITypedRequestLogEntry, depth = 0): boolean {
|
|
// Limit recursion depth to prevent stack overflow
|
|
if (depth > 3) return false;
|
|
|
|
const payload = entry.payload;
|
|
if (!payload || typeof payload !== 'object') return false;
|
|
|
|
// Check if payload looks like a TypedRequest log entry with serviceworker_* method
|
|
if (payload.method && typeof payload.method === 'string' && payload.method.startsWith('serviceworker_')) {
|
|
return true;
|
|
}
|
|
|
|
// Check nested payload
|
|
if (payload.payload) {
|
|
return this.hasNestedServiceworkerPayload({ ...entry, payload: payload.payload }, depth + 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update statistics based on new entry
|
|
*/
|
|
private updateStats(entry: interfaces.serviceworker.ITypedRequestLogEntry): void {
|
|
// Count request/response
|
|
if (entry.phase === 'request') {
|
|
this.stats.totalRequests++;
|
|
} else {
|
|
this.stats.totalResponses++;
|
|
}
|
|
|
|
// Count errors
|
|
if (entry.error) {
|
|
this.stats.errorCount++;
|
|
}
|
|
|
|
// Update duration average
|
|
if (entry.durationMs !== undefined) {
|
|
this.totalDuration += entry.durationMs;
|
|
this.durationCount++;
|
|
this.stats.avgDurationMs = Math.round(this.totalDuration / this.durationCount);
|
|
}
|
|
|
|
// Update per-method counts
|
|
if (!this.stats.methodCounts[entry.method]) {
|
|
this.stats.methodCounts[entry.method] = {
|
|
requests: 0,
|
|
responses: 0,
|
|
errors: 0,
|
|
avgDurationMs: 0,
|
|
};
|
|
}
|
|
|
|
const methodStats = this.stats.methodCounts[entry.method];
|
|
if (entry.phase === 'request') {
|
|
methodStats.requests++;
|
|
} else {
|
|
methodStats.responses++;
|
|
}
|
|
|
|
if (entry.error) {
|
|
methodStats.errors++;
|
|
}
|
|
|
|
// Per-method duration tracking (simplified - just uses latest duration)
|
|
if (entry.durationMs !== undefined) {
|
|
// Rolling average for method
|
|
const currentAvg = methodStats.avgDurationMs || 0;
|
|
const count = methodStats.responses || 1;
|
|
methodStats.avgDurationMs = Math.round((currentAvg * (count - 1) + entry.durationMs) / count);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get log entries with optional filters
|
|
*/
|
|
public getEntries(options?: {
|
|
limit?: number;
|
|
method?: string;
|
|
since?: number;
|
|
before?: number;
|
|
}): interfaces.serviceworker.ITypedRequestLogEntry[] {
|
|
let result = [...this.logs];
|
|
|
|
// Filter by method
|
|
if (options?.method) {
|
|
result = result.filter((e) => e.method === options.method);
|
|
}
|
|
|
|
// Filter by timestamp (since)
|
|
if (options?.since) {
|
|
result = result.filter((e) => e.timestamp >= options.since);
|
|
}
|
|
|
|
// Filter by timestamp (before - for pagination)
|
|
if (options?.before) {
|
|
result = result.filter((e) => e.timestamp < options.before);
|
|
}
|
|
|
|
// Sort by timestamp descending (newest first)
|
|
result.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
|
// Apply limit
|
|
if (options?.limit && options.limit > 0) {
|
|
result = result.slice(0, options.limit);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get total count of entries (for pagination)
|
|
*/
|
|
public getTotalCount(options?: { method?: string; since?: number }): number {
|
|
if (!options?.method && !options?.since) {
|
|
return this.logs.length;
|
|
}
|
|
|
|
let count = 0;
|
|
for (const entry of this.logs) {
|
|
if (options.method && entry.method !== options.method) continue;
|
|
if (options.since && entry.timestamp < options.since) continue;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Get statistics
|
|
*/
|
|
public getStats(): interfaces.serviceworker.ITypedRequestStats {
|
|
return { ...this.stats };
|
|
}
|
|
|
|
/**
|
|
* Clear all logs and reset stats
|
|
*/
|
|
public clear(): void {
|
|
this.logs = [];
|
|
this.stats = {
|
|
totalRequests: 0,
|
|
totalResponses: 0,
|
|
methodCounts: {},
|
|
errorCount: 0,
|
|
avgDurationMs: 0,
|
|
};
|
|
this.totalDuration = 0;
|
|
this.durationCount = 0;
|
|
logger.log('info', 'RequestLogStore cleared');
|
|
}
|
|
|
|
/**
|
|
* Get unique method names
|
|
*/
|
|
public getMethods(): string[] {
|
|
return Object.keys(this.stats.methodCounts);
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let requestLogStoreInstance: RequestLogStore | null = null;
|
|
|
|
/**
|
|
* Get the singleton RequestLogStore instance
|
|
*/
|
|
export function getRequestLogStore(): RequestLogStore {
|
|
if (!requestLogStoreInstance) {
|
|
requestLogStoreInstance = new RequestLogStore();
|
|
}
|
|
return requestLogStoreInstance;
|
|
}
|