adonisjs-server-stats 1.6.5 → 1.6.11

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 (86) hide show
  1. package/README.md +1 -0
  2. package/dist/core/api-client.d.ts.map +1 -1
  3. package/dist/core/dashboard-api.d.ts +2 -1
  4. package/dist/core/dashboard-api.d.ts.map +1 -1
  5. package/dist/core/dashboard-data-controller.d.ts +2 -0
  6. package/dist/core/dashboard-data-controller.d.ts.map +1 -1
  7. package/dist/core/debug-data-controller.d.ts +1 -0
  8. package/dist/core/debug-data-controller.d.ts.map +1 -1
  9. package/dist/core/history-buffer.d.ts.map +1 -1
  10. package/dist/core/index.js +404 -361
  11. package/dist/core/server-stats-controller.d.ts +1 -0
  12. package/dist/core/server-stats-controller.d.ts.map +1 -1
  13. package/dist/core/sparkline.d.ts.map +1 -1
  14. package/dist/react/core/api-client.d.ts.map +1 -1
  15. package/dist/react/core/dashboard-api.d.ts +2 -1
  16. package/dist/react/core/dashboard-api.d.ts.map +1 -1
  17. package/dist/react/core/dashboard-data-controller.d.ts +2 -0
  18. package/dist/react/core/dashboard-data-controller.d.ts.map +1 -1
  19. package/dist/react/core/debug-data-controller.d.ts +1 -0
  20. package/dist/react/core/debug-data-controller.d.ts.map +1 -1
  21. package/dist/react/core/history-buffer.d.ts.map +1 -1
  22. package/dist/react/core/server-stats-controller.d.ts +1 -0
  23. package/dist/react/core/server-stats-controller.d.ts.map +1 -1
  24. package/dist/react/core/sparkline.d.ts.map +1 -1
  25. package/dist/src/collectors/app_collector.d.ts.map +1 -1
  26. package/dist/src/collectors/db_pool_collector.d.ts.map +1 -1
  27. package/dist/src/collectors/redis_collector.d.ts.map +1 -1
  28. package/dist/src/controller/debug_controller.d.ts +3 -1
  29. package/dist/src/controller/debug_controller.d.ts.map +1 -1
  30. package/dist/src/controller/debug_controller.js +25 -20
  31. package/dist/src/dashboard/chart_aggregator.js +42 -41
  32. package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
  33. package/dist/src/dashboard/dashboard_controller.js +7 -5
  34. package/dist/src/dashboard/dashboard_store.d.ts +61 -19
  35. package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
  36. package/dist/src/dashboard/dashboard_store.js +677 -474
  37. package/dist/src/dashboard/integrations/config_inspector.d.ts +4 -0
  38. package/dist/src/dashboard/integrations/config_inspector.d.ts.map +1 -1
  39. package/dist/src/dashboard/integrations/config_inspector.js +16 -2
  40. package/dist/src/dashboard/migrator.d.ts.map +1 -1
  41. package/dist/src/dashboard/migrator.js +30 -4
  42. package/dist/src/data/data_access.d.ts.map +1 -1
  43. package/dist/src/data/data_access.js +26 -6
  44. package/dist/src/debug/debug_store.d.ts.map +1 -1
  45. package/dist/src/debug/debug_store.js +17 -7
  46. package/dist/src/debug/email_collector.d.ts +2 -0
  47. package/dist/src/debug/email_collector.d.ts.map +1 -1
  48. package/dist/src/debug/email_collector.js +17 -13
  49. package/dist/src/debug/event_collector.d.ts +7 -1
  50. package/dist/src/debug/event_collector.d.ts.map +1 -1
  51. package/dist/src/debug/event_collector.js +46 -17
  52. package/dist/src/debug/query_collector.d.ts +12 -0
  53. package/dist/src/debug/query_collector.d.ts.map +1 -1
  54. package/dist/src/debug/query_collector.js +35 -5
  55. package/dist/src/debug/ring_buffer.d.ts +14 -0
  56. package/dist/src/debug/ring_buffer.d.ts.map +1 -1
  57. package/dist/src/debug/ring_buffer.js +48 -2
  58. package/dist/src/debug/trace_collector.d.ts +1 -0
  59. package/dist/src/debug/trace_collector.d.ts.map +1 -1
  60. package/dist/src/debug/trace_collector.js +4 -1
  61. package/dist/src/define_config.d.ts.map +1 -1
  62. package/dist/src/define_config.js +5 -1
  63. package/dist/src/edge/client/dashboard.js +2 -2
  64. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  65. package/dist/src/edge/client/debug-panel.js +1 -1
  66. package/dist/src/edge/client/stats-bar.js +1 -1
  67. package/dist/src/edge/client-vue/dashboard.js +5 -5
  68. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  69. package/dist/src/edge/client-vue/debug-panel.js +2 -2
  70. package/dist/src/edge/client-vue/stats-bar.js +3 -3
  71. package/dist/src/engine/request_metrics.d.ts.map +1 -1
  72. package/dist/src/engine/request_metrics.js +33 -3
  73. package/dist/src/log_stream/log_stream_provider.js +1 -1
  74. package/dist/src/log_stream/log_stream_service.d.ts +1 -0
  75. package/dist/src/log_stream/log_stream_service.d.ts.map +1 -1
  76. package/dist/src/log_stream/log_stream_service.js +13 -3
  77. package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
  78. package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
  79. package/dist/src/provider/server_stats_provider.js +17 -31
  80. package/dist/src/stubs/config.stub +3 -0
  81. package/dist/src/types.d.ts +12 -0
  82. package/dist/src/types.d.ts.map +1 -1
  83. package/dist/src/utils/logger.d.ts +7 -5
  84. package/dist/src/utils/logger.d.ts.map +1 -1
  85. package/dist/src/utils/logger.js +27 -5
  86. package/package.json +6 -2
@@ -20,6 +20,10 @@ export interface SanitizedEnvVars {
20
20
  */
21
21
  export declare class ConfigInspector {
22
22
  private app;
23
+ private cachedConfig;
24
+ private cachedEnv;
25
+ private cacheTimestamp;
26
+ private static readonly CACHE_TTL_MS;
23
27
  constructor(app: ApplicationService);
24
28
  /**
25
29
  * Get the full application config with sensitive values redacted.
@@ -1 +1 @@
1
- {"version":3,"file":"config_inspector.d.ts","sourceRoot":"","sources":["../../../../src/dashboard/integrations/config_inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAM9D,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,CAAA;CAC5C;AA4DD;;;;;GAKG;AACH,qBAAa,eAAe;IACd,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,kBAAkB;IAE3C;;OAEG;IACH,SAAS,IAAI,eAAe;IAY5B;;OAEG;IACH,UAAU,IAAI,gBAAgB;CAmB/B"}
1
+ {"version":3,"file":"config_inspector.d.ts","sourceRoot":"","sources":["../../../../src/dashboard/integrations/config_inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAM9D,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,CAAA;CAC5C;AA4DD;;;;;GAKG;AACH,qBAAa,eAAe;IAMd,OAAO,CAAC,GAAG;IALvB,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,cAAc,CAAY;IAClC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAEzB,GAAG,EAAE,kBAAkB;IAE3C;;OAEG;IACH,SAAS,IAAI,eAAe;IAiB5B;;OAEG;IACH,UAAU,IAAI,gBAAgB;CAwB/B"}
@@ -58,6 +58,10 @@ function redact(value) {
58
58
  */
59
59
  export class ConfigInspector {
60
60
  app;
61
+ cachedConfig = null;
62
+ cachedEnv = null;
63
+ cacheTimestamp = 0;
64
+ static CACHE_TTL_MS = 30_000; // 30 seconds
61
65
  constructor(app) {
62
66
  this.app = app;
63
67
  }
@@ -65,9 +69,14 @@ export class ConfigInspector {
65
69
  * Get the full application config with sensitive values redacted.
66
70
  */
67
71
  getConfig() {
72
+ if (this.cachedConfig && Date.now() - this.cacheTimestamp < ConfigInspector.CACHE_TTL_MS) {
73
+ return this.cachedConfig;
74
+ }
68
75
  try {
69
76
  const raw = this.app.config?.all?.() ?? {};
70
- return { config: sanitizeObject(raw) };
77
+ this.cachedConfig = { config: sanitizeObject(raw) };
78
+ this.cacheTimestamp = Date.now();
79
+ return this.cachedConfig;
71
80
  }
72
81
  catch {
73
82
  return { config: {} };
@@ -77,6 +86,9 @@ export class ConfigInspector {
77
86
  * Get environment variables with sensitive values redacted.
78
87
  */
79
88
  getEnvVars() {
89
+ if (this.cachedEnv && Date.now() - this.cacheTimestamp < ConfigInspector.CACHE_TTL_MS) {
90
+ return this.cachedEnv;
91
+ }
80
92
  try {
81
93
  const env = {};
82
94
  const sorted = Object.keys(process.env).sort();
@@ -91,7 +103,9 @@ export class ConfigInspector {
91
103
  env[key] = value;
92
104
  }
93
105
  }
94
- return { env };
106
+ this.cachedEnv = { env };
107
+ this.cacheTimestamp = Date.now();
108
+ return this.cachedEnv;
95
109
  }
96
110
  catch {
97
111
  return { env: {} };
@@ -1 +1 @@
1
- {"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../../../src/dashboard/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAahC;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BxF"}
1
+ {"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../../../src/dashboard/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAahC;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAuKzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDxF"}
@@ -34,6 +34,8 @@ export async function autoMigrate(db) {
34
34
  `);
35
35
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_created ON server_stats_requests(created_at)`);
36
36
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_url ON server_stats_requests(url)`);
37
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_duration ON server_stats_requests(duration)`);
38
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_status ON server_stats_requests(status_code)`);
37
39
  await yieldToEventLoop();
38
40
  // -- server_stats_queries ---------------------------------------------------
39
41
  await db.raw(`
@@ -54,6 +56,7 @@ export async function autoMigrate(db) {
54
56
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_created ON server_stats_queries(created_at)`);
55
57
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_normalized ON server_stats_queries(sql_normalized)`);
56
58
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_request ON server_stats_queries(request_id)`);
59
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_duration ON server_stats_queries(duration)`);
57
60
  await yieldToEventLoop();
58
61
  // -- server_stats_events ----------------------------------------------------
59
62
  await db.raw(`
@@ -66,6 +69,7 @@ export async function autoMigrate(db) {
66
69
  )
67
70
  `);
68
71
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_events_created ON server_stats_events(created_at)`);
72
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_events_name ON server_stats_events(event_name)`);
69
73
  await yieldToEventLoop();
70
74
  // -- server_stats_emails ----------------------------------------------------
71
75
  await db.raw(`
@@ -118,6 +122,7 @@ export async function autoMigrate(db) {
118
122
  )
119
123
  `);
120
124
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_traces_created ON server_stats_traces(created_at)`);
125
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_traces_request ON server_stats_traces(request_id)`);
121
126
  await yieldToEventLoop();
122
127
  // -- server_stats_metrics ---------------------------------------------------
123
128
  await db.raw(`
@@ -134,6 +139,7 @@ export async function autoMigrate(db) {
134
139
  )
135
140
  `);
136
141
  await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_metrics_bucket ON server_stats_metrics(bucket)`);
142
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_metrics_created ON server_stats_metrics(created_at)`);
137
143
  await yieldToEventLoop();
138
144
  // -- server_stats_saved_filters ---------------------------------------------
139
145
  await db.raw(`
@@ -145,6 +151,7 @@ export async function autoMigrate(db) {
145
151
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
146
152
  )
147
153
  `);
154
+ await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_filters_section ON server_stats_saved_filters(section)`);
148
155
  }
149
156
  /**
150
157
  * Delete records older than `retentionDays` from all tables.
@@ -163,15 +170,34 @@ export async function runRetentionCleanup(db, retentionDays) {
163
170
  const days = Math.max(1, Math.floor(retentionDays));
164
171
  const cutoff = `datetime('now', '-${days} days')`;
165
172
  try {
173
+ // Batch deletes to avoid blocking the event loop for large tables.
174
+ // Each batch deletes up to 1000 rows, yielding between batches.
175
+ const batchDelete = async (table) => {
176
+ let deleted;
177
+ do {
178
+ const result = await db.raw(`DELETE FROM ${table} WHERE rowid IN (SELECT rowid FROM ${table} WHERE created_at < ${cutoff} LIMIT 1000)`);
179
+ deleted = result ?? 0;
180
+ // SQLite returns the number of changes via better-sqlite3's .run().changes
181
+ // but through Knex.raw() the exact shape varies — loop until no matches
182
+ const remaining = await db.raw(`SELECT COUNT(*) as cnt FROM ${table} WHERE created_at < ${cutoff} LIMIT 1`);
183
+ const cnt = remaining?.[0]?.cnt ?? 0;
184
+ if (cnt === 0)
185
+ break;
186
+ await yieldToEventLoop();
187
+ } while (true);
188
+ };
166
189
  // Cascade deletes queries, events, traces via FK ON DELETE CASCADE
167
- await db.raw(`DELETE FROM server_stats_requests WHERE created_at < ${cutoff}`);
190
+ await batchDelete('server_stats_requests');
168
191
  await yieldToEventLoop();
169
192
  // Standalone tables
170
- await db.raw(`DELETE FROM server_stats_logs WHERE created_at < ${cutoff}`);
193
+ await batchDelete('server_stats_logs');
171
194
  await yieldToEventLoop();
172
- await db.raw(`DELETE FROM server_stats_emails WHERE created_at < ${cutoff}`);
195
+ await batchDelete('server_stats_emails');
173
196
  await yieldToEventLoop();
174
- await db.raw(`DELETE FROM server_stats_metrics WHERE created_at < ${cutoff}`);
197
+ await batchDelete('server_stats_metrics');
198
+ await yieldToEventLoop();
199
+ // Reclaim space and update query planner statistics
200
+ await db.raw('PRAGMA optimize');
175
201
  }
176
202
  catch (err) {
177
203
  // Log but don't throw — retention cleanup failure shouldn't block init
@@ -1 +1 @@
1
- {"version":3,"file":"data_access.d.ts","sourceRoot":"","sources":["../../../src/data/data_access.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAMf,MAAM,iCAAiC,CAAA;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EAEX,WAAW,EACX,WAAW,EACZ,MAAM,mBAAmB,CAAA;AAM1B,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAEjC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AA6ED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAC,CAAQ;gBAGtB,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,EACrE,OAAO,CAAC,EAAE,MAAM;IAQlB,+DAA+D;IAC/D,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,wEAAwE;IACxE,OAAO,KAAK,cAAc,GAEzB;IAMK,UAAU,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB/E,eAAe,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAQrF,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB9E;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAwBjE;;;;;OAKG;IACG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAYrF;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IA+BjE;;;;OAIG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAazF;;;;;OAKG;IACH,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC;IA8BxD;;;;;;OAMG;IACG,OAAO,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAyB/D;;;;;OAKG;YACW,WAAW;CAoC1B"}
1
+ {"version":3,"file":"data_access.d.ts","sourceRoot":"","sources":["../../../src/data/data_access.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAMf,MAAM,iCAAiC,CAAA;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EAEX,WAAW,EACX,WAAW,EACZ,MAAM,mBAAmB,CAAA;AAM1B,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAEjC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AA6ED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAC,CAAQ;gBAGtB,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,EACrE,OAAO,CAAC,EAAE,MAAM;IAQlB,+DAA+D;IAC/D,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,wEAAwE;IACxE,OAAO,KAAK,cAAc,GAEzB;IAMK,UAAU,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB/E,eAAe,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAQrF,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB9E;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAqCjE;;;;;OAKG;IACG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAYrF;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAsCjE;;;;OAIG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAazF;;;;;OAKG;IACH,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC;IA8BxD;;;;;;OAMG;IACG,OAAO,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAyB/D;;;;;OAKG;YACW,WAAW;CAoC1B"}
@@ -146,8 +146,21 @@ export class DataAccess {
146
146
  return fromDashboardResult(result);
147
147
  }
148
148
  const emails = this.debugStore.emails.getEmails();
149
- // Strip html/text from list response
150
- const stripped = emails.map(({ html: _html, text: _text, ...rest }) => rest);
149
+ // Strip html/text from list response — build lightweight objects
150
+ // without object-spread to avoid copying large HTML bodies
151
+ const stripped = emails.map((e) => ({
152
+ id: e.id,
153
+ from: e.from,
154
+ to: e.to,
155
+ cc: e.cc,
156
+ bcc: e.bcc,
157
+ subject: e.subject,
158
+ mailer: e.mailer,
159
+ status: e.status,
160
+ messageId: e.messageId,
161
+ attachmentCount: e.attachmentCount,
162
+ timestamp: e.timestamp,
163
+ }));
151
164
  return wrapArray(stripped, opts, (e, term) => {
152
165
  return (e.from.toLowerCase().includes(term) ||
153
166
  e.to.toLowerCase().includes(term) ||
@@ -193,10 +206,17 @@ export class DataAccess {
193
206
  return { data: [], meta: { total: 0, page: 1, perPage: opts.perPage ?? 50, lastPage: 1 } };
194
207
  }
195
208
  const traces = this.debugStore.traces.getTraces();
196
- // Strip spans from list view, add warningCount
197
- const list = traces.map(({ spans: _spans, warnings, ...rest }) => ({
198
- ...rest,
199
- warningCount: warnings.length,
209
+ // Strip spans from list view, add warningCount — build lightweight
210
+ // objects without spread to avoid copying large span arrays
211
+ const list = traces.map((t) => ({
212
+ id: t.id,
213
+ method: t.method,
214
+ url: t.url,
215
+ statusCode: t.statusCode,
216
+ totalDuration: t.totalDuration,
217
+ spanCount: t.spanCount,
218
+ warningCount: t.warnings.length,
219
+ timestamp: t.timestamp,
200
220
  }));
201
221
  return wrapArray(list, opts, (t, term) => {
202
222
  return t.method.toLowerCase().includes(term) || t.url.toLowerCase().includes(term);
@@ -1 +1 @@
1
- {"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAA;AAEnE;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAA;gBAE1B,MAAM,EAAE,gBAAgB;IAQpC;;;OAGG;IACH,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAOpD,gEAAgE;IAChE,cAAc,IAAI;QAChB,OAAO,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACzC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KACzC;IASK,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7D,IAAI,IAAI,IAAI;IAOZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjD,uDAAuD;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAqCpD"}
1
+ {"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAA;AAEnE;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAA;gBAE1B,MAAM,EAAE,gBAAgB;IAQpC;;;OAGG;IACH,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAOpD,gEAAgE;IAChE,cAAc,IAAI;QAChB,OAAO,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACzC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KACzC;IASK,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7D,IAAI,IAAI,IAAI;IAOZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCjD,uDAAuD;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAqCpD"}
@@ -62,15 +62,25 @@ export class DebugStore {
62
62
  }
63
63
  /** Serialize all collector data to a JSON file (atomic write). */
64
64
  async saveToDisk(filePath) {
65
- const data = {
66
- queries: this.queries.getQueries(),
67
- events: this.events.getEvents(),
68
- emails: this.emails.getEmails(),
69
- };
65
+ // Build JSON incrementally to avoid a single massive JSON.stringify
66
+ // that blocks the event loop when buffers are large.
67
+ const parts = ['{'];
68
+ const queries = this.queries.getQueries();
69
+ parts.push(`"queries":${JSON.stringify(queries)},`);
70
+ // Yield between each collector's serialization to let the event loop breathe
71
+ await new Promise((resolve) => setImmediate(resolve));
72
+ const events = this.events.getEvents();
73
+ parts.push(`"events":${JSON.stringify(events)},`);
74
+ await new Promise((resolve) => setImmediate(resolve));
75
+ const emails = this.emails.getEmails();
76
+ parts.push(`"emails":${JSON.stringify(emails)}`);
70
77
  if (this.traces) {
71
- data.traces = this.traces.getTraces();
78
+ await new Promise((resolve) => setImmediate(resolve));
79
+ const traces = this.traces.getTraces();
80
+ parts.push(`,"traces":${JSON.stringify(traces)}`);
72
81
  }
73
- const json = JSON.stringify(data);
82
+ parts.push('}');
83
+ const json = parts.join('');
74
84
  const tmpPath = filePath + '.tmp';
75
85
  await mkdir(dirname(filePath), { recursive: true });
76
86
  await writeFile(tmpPath, json, 'utf-8');
@@ -9,6 +9,7 @@ import type { EmailRecord, Emitter } from './types.js';
9
9
  * - `queued:mail:error` — queued email failed
10
10
  */
11
11
  export declare class EmailCollector {
12
+ private static readonly MAX_HTML_SIZE;
12
13
  private buffer;
13
14
  private emitter;
14
15
  private handlers;
@@ -25,6 +26,7 @@ export declare class EmailCollector {
25
26
  };
26
27
  clear(): void;
27
28
  private buildRecord;
29
+ private capSize;
28
30
  /** Register a callback that fires whenever a new email is recorded. */
29
31
  onNewItem(cb: ((item: EmailRecord) => void) | null): void;
30
32
  /** Restore persisted records into the buffer and reset the ID counter. */
@@ -1 +1 @@
1
- {"version":3,"file":"email_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/email_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAA8B,MAAM,YAAY,CAAA;AAElF;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAA6D;gBAEjE,SAAS,GAAE,MAAY;IAI7B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAwD5C,IAAI,IAAI,IAAI;IAUZ,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMvC,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,WAAW;IAsBnB,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
1
+ {"version":3,"file":"email_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/email_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAA8B,MAAM,YAAY,CAAA;AAElF;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAS;IAC9C,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAA6D;gBAEjE,SAAS,GAAE,MAAY;IAI7B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAuD5C,IAAI,IAAI,IAAI;IAUZ,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKvC,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,WAAW;IAsBnB,OAAO,CAAC,OAAO;IAMf,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
@@ -10,6 +10,7 @@ import { RingBuffer } from './ring_buffer.js';
10
10
  * - `queued:mail:error` — queued email failed
11
11
  */
12
12
  export class EmailCollector {
13
+ static MAX_HTML_SIZE = 50_000; // 50 KB cap per email body
13
14
  buffer;
14
15
  emitter = null;
15
16
  handlers = [];
@@ -29,15 +30,12 @@ export class EmailCollector {
29
30
  const msg = data?.message || data;
30
31
  const to = extractAddresses(msg?.to);
31
32
  const subject = msg?.subject || '';
32
- // Try to find the matching 'sending' record and update it
33
- const all = this.buffer.toArray();
34
- for (let i = all.length - 1; i >= 0; i--) {
35
- const rec = all[i];
36
- if (rec.status === 'sending' && rec.to === to && rec.subject === subject) {
37
- rec.status = 'sent';
38
- rec.messageId = data?.response?.messageId || data?.messageId || null;
39
- return;
40
- }
33
+ // Try to find the matching 'sending' record and update it (no buffer copy)
34
+ const match = this.buffer.findFromEnd((rec) => rec.status === 'sending' && rec.to === to && rec.subject === subject);
35
+ if (match) {
36
+ match.status = 'sent';
37
+ match.messageId = data?.response?.messageId || data?.messageId || null;
38
+ return;
41
39
  }
42
40
  // No matching 'sending' record — insert a new 'sent' record
43
41
  const record = this.buildRecord(msg, 'sent', data);
@@ -80,8 +78,7 @@ export class EmailCollector {
80
78
  return this.buffer.latest(n);
81
79
  }
82
80
  getEmailHtml(id) {
83
- const all = this.buffer.toArray();
84
- const record = all.find((r) => r.id === id);
81
+ const record = this.buffer.findFromEnd((r) => r.id === id);
85
82
  return record?.html ?? null;
86
83
  }
87
84
  getTotalCount() {
@@ -101,8 +98,8 @@ export class EmailCollector {
101
98
  cc: extractAddresses(msg?.cc) || null,
102
99
  bcc: extractAddresses(msg?.bcc) || null,
103
100
  subject: msg?.subject || '(no subject)',
104
- html: msg?.html || null,
105
- text: msg?.text || null,
101
+ html: this.capSize(msg?.html),
102
+ text: this.capSize(msg?.text),
106
103
  mailer: data?.mailerName || data?.mailer || 'unknown',
107
104
  status,
108
105
  messageId: null,
@@ -110,6 +107,13 @@ export class EmailCollector {
110
107
  timestamp: Date.now(),
111
108
  };
112
109
  }
110
+ capSize(value) {
111
+ if (!value)
112
+ return null;
113
+ if (value.length <= EmailCollector.MAX_HTML_SIZE)
114
+ return value;
115
+ return value.slice(0, EmailCollector.MAX_HTML_SIZE) + '\n<!-- truncated -->';
116
+ }
113
117
  /** Register a callback that fires whenever a new email is recorded. */
114
118
  onNewItem(cb) {
115
119
  this.buffer.onPush(cb);
@@ -10,8 +10,14 @@ export declare class EventCollector {
10
10
  constructor(maxEvents?: number);
11
11
  start(emitter: Emitter): void;
12
12
  stop(): void;
13
+ /** Reusable WeakSet to avoid GC churn on every event. */
14
+ private circulars;
13
15
  private summarizeData;
14
- private safeReplacer;
16
+ /**
17
+ * Recursively limit object depth to prevent deeply-nested payloads
18
+ * from causing expensive serialization.
19
+ */
20
+ private limitDepth;
15
21
  getEvents(): EventRecord[];
16
22
  getLatest(n?: number): EventRecord[];
17
23
  getTotalCount(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"event_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/event_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEtD;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,OAAO,CAAuB;gBAE1B,SAAS,GAAE,MAAY;IAInC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAkC7B,IAAI,IAAI,IAAI;IAQZ,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,YAAY;IAapB,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
1
+ {"version":3,"file":"event_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/event_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEtD;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,OAAO,CAAuB;gBAE1B,SAAS,GAAE,MAAY;IAInC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAkC7B,IAAI,IAAI,IAAI;IAQZ,yDAAyD;IACzD,OAAO,CAAC,SAAS,CAAyB;IAE1C,OAAO,CAAC,aAAa;IAsBrB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAkClB,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
@@ -46,15 +46,22 @@ export class EventCollector {
46
46
  this.originalEmit = null;
47
47
  this.emitter = null;
48
48
  }
49
+ /** Reusable WeakSet to avoid GC churn on every event. */
50
+ circulars = new WeakSet();
49
51
  summarizeData(data) {
50
52
  if (data === undefined || data === null)
51
53
  return null;
52
54
  try {
53
55
  if (typeof data === 'string')
54
- return data;
55
- const json = JSON.stringify(data, this.safeReplacer(), 2);
56
- // Cap at 4KB per event to avoid memory bloat
57
- return json.length > 4096 ? json.slice(0, 4096) + '\n...' : json;
56
+ return data.length > 4096 ? data.slice(0, 4096) + '...' : data;
57
+ if (typeof data !== 'object')
58
+ return String(data);
59
+ // Reuse the WeakSet across calls to avoid per-event allocation
60
+ this.circulars = new WeakSet();
61
+ const limited = this.limitDepth(data, 3, this.circulars);
62
+ // Compact JSON (no indent) to reduce string size and serialization time
63
+ const result = JSON.stringify(limited) ?? '';
64
+ return result.length > 4096 ? result.slice(0, 4096) + '...' : result;
58
65
  }
59
66
  catch {
60
67
  if (typeof data === 'object' && data !== null) {
@@ -65,20 +72,42 @@ export class EventCollector {
65
72
  return typeof data;
66
73
  }
67
74
  }
68
- safeReplacer() {
69
- const seen = new WeakSet();
70
- return (_key, value) => {
71
- if (typeof value === 'object' && value !== null) {
72
- if (seen.has(value))
73
- return '[Circular]';
74
- seen.add(value);
75
- }
76
- if (typeof value === 'function')
77
- return `[Function: ${value.name || 'anonymous'}]`;
78
- if (typeof value === 'bigint')
79
- return value.toString();
75
+ /**
76
+ * Recursively limit object depth to prevent deeply-nested payloads
77
+ * from causing expensive serialization.
78
+ */
79
+ limitDepth(value, maxDepth, seen) {
80
+ if (maxDepth <= 0)
81
+ return '[...]';
82
+ if (value === null || value === undefined)
80
83
  return value;
81
- };
84
+ if (typeof value === 'function')
85
+ return `[Function: ${value.name || 'anonymous'}]`;
86
+ if (typeof value === 'bigint')
87
+ return value.toString();
88
+ if (typeof value !== 'object')
89
+ return value;
90
+ if (seen.has(value))
91
+ return '[Circular]';
92
+ seen.add(value);
93
+ if (Array.isArray(value)) {
94
+ const take = Math.min(value.length, 20);
95
+ const arr = new Array(take);
96
+ for (let i = 0; i < take; i++) {
97
+ arr[i] = this.limitDepth(value[i], maxDepth - 1, seen);
98
+ }
99
+ return arr;
100
+ }
101
+ const keys = Object.keys(value);
102
+ const result = {};
103
+ const limit = Math.min(keys.length, 50);
104
+ for (let i = 0; i < limit; i++) {
105
+ result[keys[i]] = this.limitDepth(value[keys[i]], maxDepth - 1, seen);
106
+ }
107
+ if (keys.length > 50) {
108
+ result['...'] = `(${keys.length - 50} more keys)`;
109
+ }
110
+ return result;
82
111
  }
83
112
  getEvents() {
84
113
  return this.buffer.toArray();
@@ -10,11 +10,23 @@ export declare class QueryCollector {
10
10
  private slowThresholdMs;
11
11
  private emitter;
12
12
  private handler;
13
+ private cachedSummary;
14
+ private summaryComputedAt;
13
15
  constructor(maxQueries?: number, slowThresholdMs?: number);
14
16
  start(emitter: Emitter): Promise<void>;
15
17
  stop(): void;
16
18
  getQueries(): QueryRecord[];
19
+ /**
20
+ * Get only queries with id > lastId.
21
+ * Uses collectFromEnd for O(K) performance where K = number of new items,
22
+ * instead of O(N) full buffer copy + filter.
23
+ */
24
+ getQueriesSince(lastId: number): QueryRecord[];
17
25
  getLatest(n?: number): QueryRecord[];
26
+ /**
27
+ * Cached for 1s to avoid 4 full O(N) passes over the 500-item buffer
28
+ * on every 3-second auto-refresh from the debug panel.
29
+ */
18
30
  getSummary(): {
19
31
  total: number;
20
32
  slow: number;
@@ -1 +1 @@
1
- {"version":3,"file":"query_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/query_collector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAA;AAEpE;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAA8C;gBAEjD,UAAU,GAAE,MAAY,EAAE,eAAe,GAAE,MAAY;IAK7D,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC5C,IAAI,IAAI,IAAI;IAQZ,UAAU,IAAI,WAAW,EAAE;IAI3B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,UAAU;;;;;;IAqBV,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
1
+ {"version":3,"file":"query_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/query_collector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAA;AAEpE;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,aAAa,CAKN;IACf,OAAO,CAAC,iBAAiB,CAAY;gBAEzB,UAAU,GAAE,MAAY,EAAE,eAAe,GAAE,MAAY;IAK7D,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC5C,IAAI,IAAI,IAAI;IAQZ,UAAU,IAAI,WAAW,EAAE;IAI3B;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE;IAK9C,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC;;;OAGG;IACH,UAAU;eA7ED,MAAM;cACP,MAAM;oBACA,MAAM;qBACL,MAAM;;IA4GrB,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
@@ -12,6 +12,8 @@ export class QueryCollector {
12
12
  slowThresholdMs;
13
13
  emitter = null;
14
14
  handler = null;
15
+ cachedSummary = null;
16
+ summaryComputedAt = 0;
15
17
  constructor(maxQueries = 500, slowThresholdMs = 100) {
16
18
  this.buffer = new RingBuffer(maxQueries);
17
19
  this.slowThresholdMs = slowThresholdMs;
@@ -57,25 +59,53 @@ export class QueryCollector {
57
59
  getQueries() {
58
60
  return this.buffer.toArray();
59
61
  }
62
+ /**
63
+ * Get only queries with id > lastId.
64
+ * Uses collectFromEnd for O(K) performance where K = number of new items,
65
+ * instead of O(N) full buffer copy + filter.
66
+ */
67
+ getQueriesSince(lastId) {
68
+ if (lastId <= 0)
69
+ return this.buffer.toArray();
70
+ return this.buffer.collectFromEnd((q) => q.id > lastId);
71
+ }
60
72
  getLatest(n = 100) {
61
73
  return this.buffer.latest(n);
62
74
  }
75
+ /**
76
+ * Cached for 1s to avoid 4 full O(N) passes over the 500-item buffer
77
+ * on every 3-second auto-refresh from the debug panel.
78
+ */
63
79
  getSummary() {
80
+ const now = Date.now();
81
+ if (this.cachedSummary && now - this.summaryComputedAt < 1000) {
82
+ return this.cachedSummary;
83
+ }
84
+ // Single pass over the buffer to compute all metrics at once
64
85
  const queries = this.buffer.toArray();
65
86
  const total = queries.length;
66
- const slow = queries.filter((q) => q.duration > this.slowThresholdMs).length;
87
+ let slow = 0;
88
+ let totalDuration = 0;
67
89
  const sqlCounts = new Map();
68
90
  for (const q of queries) {
91
+ if (q.duration > this.slowThresholdMs)
92
+ slow++;
93
+ totalDuration += q.duration;
69
94
  sqlCounts.set(q.sql, (sqlCounts.get(q.sql) || 0) + 1);
70
95
  }
71
- const duplicates = Array.from(sqlCounts.values()).filter((c) => c > 1).length;
72
- const avgDuration = total > 0 ? queries.reduce((sum, q) => sum + q.duration, 0) / total : 0;
73
- return {
96
+ let duplicates = 0;
97
+ for (const count of sqlCounts.values()) {
98
+ if (count > 1)
99
+ duplicates++;
100
+ }
101
+ this.cachedSummary = {
74
102
  total,
75
103
  slow,
76
104
  duplicates,
77
- avgDuration: round(avgDuration),
105
+ avgDuration: total > 0 ? round(totalDuration / total) : 0,
78
106
  };
107
+ this.summaryComputedAt = now;
108
+ return this.cachedSummary;
79
109
  }
80
110
  getTotalCount() {
81
111
  return this.buffer.size();
@@ -21,6 +21,20 @@ export declare class RingBuffer<T> {
21
21
  size(): number;
22
22
  getCapacity(): number;
23
23
  clear(): void;
24
+ /**
25
+ * Find a single item by predicate, searching from newest to oldest.
26
+ * Returns immediately on first match without copying the buffer.
27
+ */
28
+ findFromEnd(predicate: (item: T) => boolean): T | undefined;
29
+ /**
30
+ * Collect items from the end of the buffer while the predicate holds.
31
+ * Iterates from newest to oldest and stops at the first non-match.
32
+ * Returns items in insertion order (oldest first).
33
+ *
34
+ * Useful for efficiently getting "items since ID X" without copying the
35
+ * entire buffer, since IDs are monotonically increasing.
36
+ */
37
+ collectFromEnd(predicate: (item: T) => boolean): T[];
24
38
  /** Bulk-load items (e.g. from disk). Pushes each in order, respecting capacity. */
25
39
  load(items: T[]): void;
26
40
  /** Restore the auto-increment counter (e.g. after loading persisted data). */
@@ -1 +1 @@
1
- {"version":3,"file":"ring_buffer.d.ts","sourceRoot":"","sources":["../../../src/debug/ring_buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,CAAC;IAOX,OAAO,CAAC,QAAQ;IAN5B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,YAAY,CAAmC;gBAEnC,QAAQ,EAAE,MAAM;IAIpC,oEAAoE;IACpE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAI5C,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IASnB,2DAA2D;IAC3D,OAAO,IAAI,CAAC,EAAE;IAcd,sDAAsD;IACtD,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;IAKtB,SAAS,IAAI,MAAM;IAInB,IAAI,IAAI,MAAM;IAId,WAAW,IAAI,MAAM;IAIrB,KAAK,IAAI,IAAI;IAMb,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAMtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAG5B"}
1
+ {"version":3,"file":"ring_buffer.d.ts","sourceRoot":"","sources":["../../../src/debug/ring_buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,CAAC;IAOX,OAAO,CAAC,QAAQ;IAN5B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,YAAY,CAAmC;gBAEnC,QAAQ,EAAE,MAAM;IAIpC,oEAAoE;IACpE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAI5C,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IASnB,2DAA2D;IAC3D,OAAO,IAAI,CAAC,EAAE;IAcd,sDAAsD;IACtD,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;IActB,SAAS,IAAI,MAAM;IAInB,IAAI,IAAI,MAAM;IAId,WAAW,IAAI,MAAM;IAIrB,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,WAAW,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,SAAS;IAc3D;;;;;;;OAOG;IACH,cAAc,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,EAAE;IAgBpD,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAMtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAG5B"}