@kysera/debug 0.7.2 → 0.7.4

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/src/plugin.ts CHANGED
@@ -11,24 +11,14 @@ import type {
11
11
  QueryResult,
12
12
  UnknownRow,
13
13
  KyselyPlugin,
14
- RootOperationNode,
15
- } from 'kysely';
16
- import { DefaultQueryCompiler } from 'kysely';
17
- import { consoleLogger, type KyseraLogger } from '@kysera/core';
14
+ RootOperationNode
15
+ } from 'kysely'
16
+ import { DefaultQueryCompiler } from 'kysely'
17
+ import { consoleLogger, type KyseraLogger, type QueryMetrics } from '@kysera/core'
18
+ import { CircularBuffer } from './circular-buffer.js'
18
19
 
19
- /**
20
- * Query metrics data.
21
- */
22
- export interface QueryMetrics {
23
- /** SQL query string */
24
- sql: string;
25
- /** Query parameters */
26
- params?: unknown[];
27
- /** Query execution duration in milliseconds */
28
- duration: number;
29
- /** Timestamp when query was executed */
30
- timestamp: number;
31
- }
20
+ // Re-export QueryMetrics for backwards compatibility
21
+ export type { QueryMetrics }
32
22
 
33
23
  /**
34
24
  * Options for debug plugin.
@@ -38,37 +28,37 @@ export interface DebugOptions {
38
28
  * Log query SQL.
39
29
  * @default true
40
30
  */
41
- logQuery?: boolean;
31
+ logQuery?: boolean
42
32
 
43
33
  /**
44
34
  * Log query parameters.
45
35
  * @default false
46
36
  */
47
- logParams?: boolean;
37
+ logParams?: boolean
48
38
 
49
39
  /**
50
40
  * Duration threshold (ms) to consider a query slow.
51
41
  * @default 100
52
42
  */
53
- slowQueryThreshold?: number;
43
+ slowQueryThreshold?: number
54
44
 
55
45
  /**
56
46
  * Callback for slow queries.
57
47
  */
58
- onSlowQuery?: (sql: string, duration: number) => void;
48
+ onSlowQuery?: (sql: string, duration: number) => void
59
49
 
60
50
  /**
61
51
  * Logger for debug messages.
62
52
  * @default consoleLogger
63
53
  */
64
- logger?: KyseraLogger;
54
+ logger?: KyseraLogger
65
55
 
66
56
  /**
67
57
  * Maximum number of metrics to keep in memory.
68
58
  * When limit is reached, oldest metrics are removed (circular buffer).
69
59
  * @default 1000
70
60
  */
71
- maxMetrics?: number;
61
+ maxMetrics?: number
72
62
  }
73
63
 
74
64
  /**
@@ -76,9 +66,9 @@ export interface DebugOptions {
76
66
  * @internal
77
67
  */
78
68
  interface QueryData {
79
- startTime: number;
80
- sql: string;
81
- params: readonly unknown[];
69
+ startTime: number
70
+ sql: string
71
+ params: readonly unknown[]
82
72
  }
83
73
 
84
74
  /**
@@ -86,89 +76,87 @@ interface QueryData {
86
76
  * @internal
87
77
  */
88
78
  class DebugPlugin implements KyselyPlugin {
89
- private metrics: QueryMetrics[] = [];
90
- private queryData = new WeakMap<object, QueryData>();
91
- private readonly maxMetrics: number;
92
- private readonly logger: KyseraLogger;
93
- private readonly options: Required<Pick<DebugOptions, 'logQuery' | 'logParams' | 'slowQueryThreshold'>>;
94
- private readonly onSlowQuery: ((sql: string, duration: number) => void) | undefined;
79
+ private readonly metricsBuffer: CircularBuffer<QueryMetrics>
80
+ private queryData = new WeakMap<object, QueryData>()
81
+ private readonly logger: KyseraLogger
82
+ private readonly options: Required<
83
+ Pick<DebugOptions, 'logQuery' | 'logParams' | 'slowQueryThreshold'>
84
+ >
85
+ private readonly onSlowQuery: ((sql: string, duration: number) => void) | undefined
95
86
 
96
87
  constructor(options: DebugOptions = {}) {
97
- this.logger = options.logger ?? consoleLogger;
98
- this.maxMetrics = options.maxMetrics ?? 1000;
99
- this.onSlowQuery = options.onSlowQuery;
88
+ this.logger = options.logger ?? consoleLogger
89
+ this.metricsBuffer = new CircularBuffer<QueryMetrics>(options.maxMetrics ?? 1000)
90
+ this.onSlowQuery = options.onSlowQuery
100
91
  this.options = {
101
92
  logQuery: options.logQuery ?? true,
102
93
  logParams: options.logParams ?? false,
103
- slowQueryThreshold: options.slowQueryThreshold ?? 100,
104
- };
94
+ slowQueryThreshold: options.slowQueryThreshold ?? 100
95
+ }
105
96
  }
106
97
 
107
98
  transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
108
- const startTime = performance.now();
99
+ const startTime = performance.now()
109
100
 
110
101
  // Compile the query to get SQL and parameters
111
- const compiler = new DefaultQueryCompiler();
112
- const compiled = compiler.compileQuery(args.node, args.queryId);
102
+ const compiler = new DefaultQueryCompiler()
103
+ const compiled = compiler.compileQuery(args.node, args.queryId)
113
104
 
114
105
  // Store query data for later use in transformResult
115
106
  this.queryData.set(args.queryId, {
116
107
  startTime,
117
108
  sql: compiled.sql,
118
- params: compiled.parameters,
119
- });
109
+ params: compiled.parameters
110
+ })
120
111
 
121
- return args.node;
112
+ return args.node
122
113
  }
123
114
 
124
115
  transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
125
- const data = this.queryData.get(args.queryId);
116
+ const data = this.queryData.get(args.queryId)
126
117
 
127
118
  if (data) {
128
- const endTime = performance.now();
129
- const duration = endTime - data.startTime;
130
- this.queryData.delete(args.queryId);
119
+ const endTime = performance.now()
120
+ const duration = endTime - data.startTime
121
+ this.queryData.delete(args.queryId)
131
122
 
132
123
  const metric: QueryMetrics = {
133
124
  sql: data.sql,
134
125
  params: [...data.params],
135
126
  duration,
136
- timestamp: Date.now(),
137
- };
138
-
139
- // Circular buffer: keep only last N metrics
140
- this.metrics.push(metric);
141
- if (this.metrics.length > this.maxMetrics) {
142
- this.metrics.shift();
127
+ timestamp: Date.now()
143
128
  }
144
129
 
130
+ // Add to circular buffer (O(1) operation)
131
+ this.metricsBuffer.add(metric)
132
+
145
133
  if (this.options.logQuery) {
146
134
  const message = this.options.logParams
147
- ? `[SQL] ${data.sql}\n[Params] ${JSON.stringify(data.params)}`
148
- : `[SQL] ${data.sql}`;
149
- this.logger.debug(message);
150
- this.logger.debug(`[Duration] ${duration.toFixed(2)}ms`);
135
+ ? '[SQL] ' + data.sql + '\n[Params] ' + JSON.stringify(data.params)
136
+ : '[SQL] ' + data.sql
137
+ this.logger.debug(message)
138
+ this.logger.debug('[Duration] ' + duration.toFixed(2) + 'ms')
151
139
  }
152
140
 
153
141
  // Check for slow query
154
142
  if (duration > this.options.slowQueryThreshold) {
155
143
  if (this.onSlowQuery) {
156
- this.onSlowQuery(data.sql, duration);
144
+ this.onSlowQuery(data.sql, duration)
157
145
  } else {
158
- this.logger.warn(`[SLOW QUERY] ${duration.toFixed(2)}ms: ${data.sql}`);
146
+ this.logger.warn('[SLOW QUERY] ' + duration.toFixed(2) + 'ms: ' + data.sql)
159
147
  }
160
148
  }
161
149
  }
162
150
 
163
- return Promise.resolve(args.result);
151
+ return Promise.resolve(args.result)
164
152
  }
165
153
 
166
154
  getMetrics(): QueryMetrics[] {
167
- return [...this.metrics];
155
+ return this.metricsBuffer.getOrdered()
168
156
  }
169
157
 
170
158
  clearMetrics(): void {
171
- this.metrics = [];
159
+ this.metricsBuffer.clear()
172
160
  }
173
161
  }
174
162
 
@@ -177,9 +165,9 @@ class DebugPlugin implements KyselyPlugin {
177
165
  */
178
166
  export interface DebugDatabase<DB> extends Kysely<DB> {
179
167
  /** Get all collected query metrics */
180
- getMetrics(): QueryMetrics[];
168
+ getMetrics(): QueryMetrics[]
181
169
  /** Clear all collected metrics */
182
- clearMetrics(): void;
170
+ clearMetrics(): void
183
171
  }
184
172
 
185
173
  /**
@@ -202,7 +190,7 @@ export interface DebugDatabase<DB> extends Kysely<DB> {
202
190
  *
203
191
  * // Get collected metrics
204
192
  * const metrics = debugDb.getMetrics();
205
- * console.log(`Total queries: ${metrics.length}`);
193
+ * console.log('Total queries: ' + metrics.length);
206
194
  * ```
207
195
  *
208
196
  * @example With custom options
@@ -215,23 +203,20 @@ export interface DebugDatabase<DB> extends Kysely<DB> {
215
203
  * slowQueryThreshold: 50,
216
204
  * maxMetrics: 500,
217
205
  * onSlowQuery: (sql, duration) => {
218
- * alertService.notify(`Slow query: ${duration}ms`);
206
+ * alertService.notify('Slow query: ' + duration + 'ms');
219
207
  * },
220
208
  * });
221
209
  * ```
222
210
  */
223
- export function withDebug<DB>(
224
- db: Kysely<DB>,
225
- options: DebugOptions = {}
226
- ): DebugDatabase<DB> {
227
- const plugin = new DebugPlugin(options);
228
- const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>;
211
+ export function withDebug<DB>(db: Kysely<DB>, options: DebugOptions = {}): DebugDatabase<DB> {
212
+ const plugin = new DebugPlugin(options)
213
+ const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>
229
214
 
230
215
  // Attach metrics methods
231
- debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics();
216
+ debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics()
232
217
  debugDb.clearMetrics = (): void => {
233
- plugin.clearMetrics();
234
- };
218
+ plugin.clearMetrics()
219
+ }
235
220
 
236
- return debugDb;
221
+ return debugDb
237
222
  }
package/src/profiler.ts CHANGED
@@ -4,24 +4,25 @@
4
4
  * @module @kysera/debug
5
5
  */
6
6
 
7
- import type { QueryMetrics } from './plugin.js';
7
+ import type { QueryMetrics } from '@kysera/core'
8
+ import { CircularBuffer } from './circular-buffer.js'
8
9
 
9
10
  /**
10
11
  * Query profiler summary.
11
12
  */
12
13
  export interface ProfilerSummary {
13
14
  /** Total number of recorded queries */
14
- totalQueries: number;
15
+ totalQueries: number
15
16
  /** Sum of all query durations */
16
- totalDuration: number;
17
+ totalDuration: number
17
18
  /** Average query duration */
18
- averageDuration: number;
19
+ averageDuration: number
19
20
  /** Slowest recorded query */
20
- slowestQuery: QueryMetrics | null;
21
+ slowestQuery: QueryMetrics | null
21
22
  /** Fastest recorded query */
22
- fastestQuery: QueryMetrics | null;
23
+ fastestQuery: QueryMetrics | null
23
24
  /** All recorded queries */
24
- queries: QueryMetrics[];
25
+ queries: QueryMetrics[]
25
26
  }
26
27
 
27
28
  /**
@@ -32,7 +33,7 @@ export interface ProfilerOptions {
32
33
  * Maximum number of queries to keep in memory.
33
34
  * @default 1000
34
35
  */
35
- maxQueries?: number;
36
+ maxQueries?: number
36
37
  }
37
38
 
38
39
  /**
@@ -56,16 +57,15 @@ export interface ProfilerOptions {
56
57
  *
57
58
  * // Get summary
58
59
  * const summary = profiler.getSummary();
59
- * console.log(`Total queries: ${summary.totalQueries}`);
60
- * console.log(`Average duration: ${summary.averageDuration.toFixed(2)}ms`);
60
+ * console.log('Total queries: ' + summary.totalQueries);
61
+ * console.log('Average duration: ' + summary.averageDuration.toFixed(2) + 'ms');
61
62
  *
62
63
  * // Clear recorded queries
63
64
  * profiler.clear();
64
65
  * ```
65
66
  */
66
67
  export class QueryProfiler {
67
- private queries: QueryMetrics[] = [];
68
- private readonly maxQueries: number;
68
+ private readonly queriesBuffer: CircularBuffer<QueryMetrics>
69
69
 
70
70
  /**
71
71
  * Create a new query profiler.
@@ -73,20 +73,19 @@ export class QueryProfiler {
73
73
  * @param options - Profiler options
74
74
  */
75
75
  constructor(options: ProfilerOptions = {}) {
76
- this.maxQueries = options.maxQueries ?? 1000;
76
+ this.queriesBuffer = new CircularBuffer<QueryMetrics>(options.maxQueries ?? 1000)
77
77
  }
78
78
 
79
79
  /**
80
80
  * Record a query metric.
81
81
  *
82
+ * Uses O(1) circular buffer to maintain bounded memory usage.
83
+ * When the buffer is full, oldest entries are overwritten.
84
+ *
82
85
  * @param metric - Query metrics to record
83
86
  */
84
87
  record(metric: QueryMetrics): void {
85
- this.queries.push(metric);
86
- // Circular buffer: keep only last N queries
87
- if (this.queries.length > this.maxQueries) {
88
- this.queries.shift();
89
- }
88
+ this.queriesBuffer.add(metric)
90
89
  }
91
90
 
92
91
  /**
@@ -95,28 +94,30 @@ export class QueryProfiler {
95
94
  * @returns Summary of all recorded queries
96
95
  */
97
96
  getSummary(): ProfilerSummary {
98
- if (this.queries.length === 0) {
97
+ const orderedQueries = this.getOrderedQueries()
98
+
99
+ if (orderedQueries.length === 0) {
99
100
  return {
100
101
  totalQueries: 0,
101
102
  totalDuration: 0,
102
103
  averageDuration: 0,
103
104
  slowestQuery: null,
104
105
  fastestQuery: null,
105
- queries: [],
106
- };
106
+ queries: []
107
+ }
107
108
  }
108
109
 
109
- const totalDuration = this.queries.reduce((sum, q) => sum + q.duration, 0);
110
- const sorted = [...this.queries].sort((a, b) => b.duration - a.duration);
110
+ const totalDuration = orderedQueries.reduce((sum, q) => sum + q.duration, 0)
111
+ const sorted = [...orderedQueries].sort((a, b) => b.duration - a.duration)
111
112
 
112
113
  return {
113
- totalQueries: this.queries.length,
114
+ totalQueries: orderedQueries.length,
114
115
  totalDuration,
115
- averageDuration: totalDuration / this.queries.length,
116
+ averageDuration: totalDuration / orderedQueries.length,
116
117
  slowestQuery: sorted[0] ?? null,
117
118
  fastestQuery: sorted[sorted.length - 1] ?? null,
118
- queries: [...this.queries],
119
- };
119
+ queries: orderedQueries
120
+ }
120
121
  }
121
122
 
122
123
  /**
@@ -126,9 +127,7 @@ export class QueryProfiler {
126
127
  * @returns Array of slowest queries
127
128
  */
128
129
  getSlowestQueries(count: number): QueryMetrics[] {
129
- return [...this.queries]
130
- .sort((a, b) => b.duration - a.duration)
131
- .slice(0, count);
130
+ return [...this.getOrderedQueries()].sort((a, b) => b.duration - a.duration).slice(0, count)
132
131
  }
133
132
 
134
133
  /**
@@ -138,20 +137,30 @@ export class QueryProfiler {
138
137
  * @returns Array of slow queries
139
138
  */
140
139
  getSlowQueries(thresholdMs: number): QueryMetrics[] {
141
- return this.queries.filter((q) => q.duration > thresholdMs);
140
+ return this.getOrderedQueries().filter(q => q.duration > thresholdMs)
142
141
  }
143
142
 
144
143
  /**
145
144
  * Clear all recorded queries.
146
145
  */
147
146
  clear(): void {
148
- this.queries = [];
147
+ this.queriesBuffer.clear()
149
148
  }
150
149
 
151
150
  /**
152
151
  * Get the number of recorded queries.
153
152
  */
154
153
  get count(): number {
155
- return this.queries.length;
154
+ return this.queriesBuffer.size
155
+ }
156
+
157
+ /**
158
+ * Get queries in chronological order.
159
+ * Handles circular buffer wrap-around correctly.
160
+ *
161
+ * @returns Array of queries in chronological order
162
+ */
163
+ private getOrderedQueries(): QueryMetrics[] {
164
+ return this.queriesBuffer.getOrdered()
156
165
  }
157
166
  }