thatcher 1.0.41 → 1.0.43

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 (122) hide show
  1. package/README.md +27 -0
  2. package/package.json +2 -5
  3. package/src/app/api/audit/dashboard/route.js +2 -2
  4. package/src/app/api/audit/logs/route.js +2 -2
  5. package/src/app/api/audit/permissions/[id]/route.js +2 -2
  6. package/src/app/api/audit/permissions/route.js +3 -3
  7. package/src/app/api/audit/permissions/stats/route.js +2 -2
  8. package/src/app/api/audit/route.js +2 -3
  9. package/src/app/api/audit/stats/route.js +2 -2
  10. package/src/app/api/auth/google/callback/route.js +1 -1
  11. package/src/app/api/auth/google/route.js +1 -1
  12. package/src/app/api/auth/login/route.js +1 -1
  13. package/src/app/api/auth/logout/route.js +1 -1
  14. package/src/app/api/auth/me/route.js +1 -1
  15. package/src/app/api/auth/mwr-bridge/route.js +1 -1
  16. package/src/app/api/auth/password-reset/route.js +1 -1
  17. package/src/app/api/csrf-token/route.js +1 -1
  18. package/src/app/api/debug/[[...path]]/route.js +2 -2
  19. package/src/app/api/debug/config/route.js +1 -1
  20. package/src/app/api/debug/hooks/route.js +1 -1
  21. package/src/app/api/debug/plugins/route.js +1 -1
  22. package/src/app/api/debug/sqlite/route.js +1 -1
  23. package/src/app/api/debug/sync/route.js +1 -1
  24. package/src/app/api/debug/workflow/route.js +1 -1
  25. package/src/app/api/domains/[domain]/route.js +1 -2
  26. package/src/app/api/domains/route.js +1 -1
  27. package/src/app/api/email/allocate/batch/route.js +1 -1
  28. package/src/app/api/email/allocate/route.js +1 -1
  29. package/src/app/api/email/receive/route.js +1 -1
  30. package/src/app/api/email/send/route.js +1 -1
  31. package/src/app/api/email/unallocated/route.js +1 -1
  32. package/src/app/api/files/[id]/route.js +1 -1
  33. package/src/app/api/health/route.js +1 -2
  34. package/src/app/api/metrics/route.js +1 -2
  35. package/src/engine.js +3 -3
  36. package/src/engine.server.js +3 -3
  37. package/src/index.js +35 -10
  38. package/src/lib/action-factory.js +1 -1
  39. package/src/lib/api-helpers.js +1 -1
  40. package/src/lib/api.js +3 -4
  41. package/src/lib/audit-logger-enhanced.js +1 -1
  42. package/src/lib/auth-middleware.js +1 -1
  43. package/src/lib/auth-route-helpers.js +1 -1
  44. package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
  45. package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
  46. package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
  47. package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
  48. package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
  49. package/src/lib/config-generator-engine.js +13 -1
  50. package/src/lib/crud-action-helpers.js +1 -1
  51. package/src/lib/crud-handlers.js +4 -4
  52. package/src/lib/email-sender.js +1 -1
  53. package/src/lib/errors/index.js +3 -0
  54. package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
  55. package/src/lib/{error-handler.js → errors/types.js} +1 -1
  56. package/src/lib/errors/wrap.js +225 -0
  57. package/src/lib/events-engine.js +1 -1
  58. package/src/lib/hot-reload/index.js +34 -0
  59. package/src/lib/index.js +5 -5
  60. package/src/lib/keyed-cache.js +112 -0
  61. package/src/lib/monitor.js +285 -0
  62. package/src/lib/next-shim.js +294 -0
  63. package/src/lib/observability-init.js +213 -0
  64. package/src/lib/perf.js +468 -0
  65. package/src/lib/query-cache.js +40 -46
  66. package/src/lib/realtime-server.js +18 -0
  67. package/src/lib/render-cache.js +14 -28
  68. package/src/lib/{request-tracing.js → request-trace.js} +57 -2
  69. package/src/lib/response-formatter.js +18 -0
  70. package/src/lib/route-helpers.js +1 -1
  71. package/src/lib/state-protocol.js +13 -0
  72. package/src/lib/state-transport-client.js +6 -0
  73. package/src/lib/state-transport-reconnect.js +5 -0
  74. package/src/lib/state-transport-server.js +8 -0
  75. package/src/lib/utils.js +1 -1
  76. package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
  77. package/src/lib/validation/index.js +1 -1
  78. package/src/lib/validation/security-validators.js +1 -1
  79. package/src/lib/validation-middleware.js +2 -2
  80. package/src/lib/workflow-engine.js +23 -3
  81. package/src/lib/xstate-workflow-engine.js +18 -2
  82. package/src/services/collaborator-role.service.js +2 -2
  83. package/src/services/notification-engine.js +5 -5
  84. package/src/services/permission.service.js +1 -1
  85. package/src/ui/format-helpers.js +1 -4
  86. package/src/ui/highlight-threading-renderer.js +1 -1
  87. package/src/ui/page-handler-admin.js +6 -5
  88. package/src/ui/page-handler-helpers.js +2 -2
  89. package/src/ui/page-handler-reviews.js +4 -4
  90. package/src/ui/page-handler-rfi.js +1 -1
  91. package/src/ui/page-handler.js +2 -2
  92. package/src/ui/render-helpers.js +1 -1
  93. package/src/ui/renderer.js +1 -1
  94. package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
  95. package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
  96. package/src/ui/review/index.js +32 -0
  97. package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
  98. package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
  99. package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
  100. package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
  101. package/src/ui/settings/shared.js +45 -0
  102. package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
  103. package/src/ui/settings/templates.js +116 -0
  104. package/src/lib/api-error-wrapper.js +0 -125
  105. package/src/lib/db-monitor.js +0 -127
  106. package/src/lib/error-resilience.js +0 -132
  107. package/src/lib/monitoring-init.js +0 -67
  108. package/src/lib/next-compat.js +0 -94
  109. package/src/lib/next-polyfills.js +0 -135
  110. package/src/lib/observability-bootstrap.js +0 -116
  111. package/src/lib/perf-monitor.js +0 -94
  112. package/src/lib/perf-profiler.js +0 -233
  113. package/src/lib/query-perf.js +0 -117
  114. package/src/lib/request-tracker.js +0 -43
  115. package/src/lib/resource-monitor.js +0 -120
  116. package/src/lib/validators.js +0 -67
  117. package/src/lib/with-error-handler.js +0 -31
  118. package/src/ui/settings-renderer-advanced2.js +0 -159
  119. /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
  120. /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
  121. /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
  122. /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
@@ -0,0 +1,213 @@
1
+ // Merged from monitoring-init.js (resource-monitoring + alert-threshold-check
2
+ // loop) and observability-bootstrap.js (tracing/profiler/debug-registry/
3
+ // export-sink/hook-tracing wiring) into one init entry point. Both source
4
+ // files had zero external call sites at merge time (confirmed by grep) — a
5
+ // future caller wanting "start everything observability-related" now has one
6
+ // function, `initializeObservability`, that runs both halves; the original
7
+ // two function names (`initializeMonitoring`/`bootstrapObservability`) are
8
+ // kept as exports too so a caller wanting only one half still can.
9
+
10
+ import { tracer } from './tracing.js';
11
+ import { perfProfiler } from './perf.js';
12
+ import { debugRegistry } from './debug-registry.js';
13
+ import { createExportSink } from './export-sink.js';
14
+ import { withHookTracing } from './request-trace.js';
15
+ import { hookEngine } from './hook-engine.js';
16
+ import { createLogger } from './logger.js';
17
+ import { startMonitoring as startResourceMonitoring, stopMonitoring as stopResourceMonitoring } from './monitor.js';
18
+ import { checkAllThresholds, registerAlertHandler } from './alert-manager.js';
19
+ import { getAllMetrics } from './metrics-collector.js';
20
+ import { info, warn, error } from './log-aggregator.js';
21
+ import path from 'path';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const logger = createLogger('[ObservabilityInit]');
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+
27
+ let alertCheckInterval = null;
28
+ let monitoringInitialized = false;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // From monitoring-init.js — resource monitoring + alert-threshold checking.
32
+ // ---------------------------------------------------------------------------
33
+ export function initializeMonitoring(config = {}) {
34
+ if (monitoringInitialized) {
35
+ warn('Monitoring already initialized');
36
+ return;
37
+ }
38
+
39
+ const {
40
+ resourceInterval = 5000,
41
+ alertCheckInterval: alertInterval = 10000,
42
+ dbPath = path.join(__dirname, '../../data/app.db')
43
+ } = config;
44
+
45
+ startResourceMonitoring(resourceInterval, dbPath);
46
+ info('Resource monitoring started', { interval: resourceInterval });
47
+
48
+ alertCheckInterval = setInterval(() => {
49
+ try {
50
+ const metrics = getAllMetrics();
51
+ checkAllThresholds(metrics);
52
+ } catch (err) {
53
+ error('Alert check failed', { error: err.message });
54
+ }
55
+ }, alertInterval);
56
+
57
+ info('Alert checking started', { interval: alertInterval });
58
+
59
+ registerAlertHandler((alert) => {
60
+ if (alert.severity === 'critical') {
61
+ error(`CRITICAL ALERT: ${alert.message}`, alert.metadata);
62
+ } else if (alert.severity === 'warning') {
63
+ warn(`WARNING: ${alert.message}`, alert.metadata);
64
+ }
65
+ });
66
+
67
+ info('Alert handlers registered');
68
+
69
+ monitoringInitialized = true;
70
+ info('Monitoring system initialized');
71
+ }
72
+
73
+ export async function shutdownMonitoring() {
74
+ if (!monitoringInitialized) return;
75
+
76
+ if (alertCheckInterval) {
77
+ clearInterval(alertCheckInterval);
78
+ alertCheckInterval = null;
79
+ }
80
+
81
+ stopResourceMonitoring();
82
+
83
+ monitoringInitialized = false;
84
+ info('Monitoring system shutdown');
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // From observability-bootstrap.js — tracing/profiler/debug-registry/export
89
+ // sink/hook-tracing wiring.
90
+ // ---------------------------------------------------------------------------
91
+ export async function bootstrapObservability(options = {}) {
92
+ const results = {
93
+ tracing: false,
94
+ profiler: false,
95
+ registry: false,
96
+ export: false,
97
+ hookTracing: false,
98
+ errors: [],
99
+ };
100
+
101
+ try {
102
+ debugRegistry.expose('system', {
103
+ pid: () => process.pid,
104
+ uptime: () => process.uptime(),
105
+ memory: () => process.memoryUsage(),
106
+ cpuUsage: () => process.cpuUsage(),
107
+ version: () => process.version,
108
+ platform: () => process.platform,
109
+ }, 'System information');
110
+
111
+ results.registry = true;
112
+ logger.info('Debug registry initialized');
113
+ } catch (error) {
114
+ results.errors.push({ subsystem: 'registry', error: error.message });
115
+ logger.error('Registry bootstrap failed', { error: error.message });
116
+ }
117
+
118
+ try {
119
+ if (globalThis.__debug__) {
120
+ globalThis.__debug__.expose('tracing', {
121
+ activeSpans: () => tracer.getActiveSpans(),
122
+ recentTraces: (limit) => tracer.getRecentTraces(limit),
123
+ traceById: (id) => tracer.getTraceById(id),
124
+ stats: () => tracer.getStats(),
125
+ currentTraceId: () => tracer.getCurrentTraceId(),
126
+ }, 'Tracing Core');
127
+ }
128
+
129
+ results.tracing = true;
130
+ logger.info('Tracing initialized');
131
+ } catch (error) {
132
+ results.errors.push({ subsystem: 'tracing', error: error.message });
133
+ logger.error('Tracing bootstrap failed', { error: error.message });
134
+ }
135
+
136
+ try {
137
+ if (globalThis.__debug__) {
138
+ globalThis.__debug__.expose('perf', {
139
+ stats: (op) => op ? perfProfiler.getStats(op) : perfProfiler.getAllStats(),
140
+ slow: () => perfProfiler.getSlowOperations(),
141
+ regressions: () => perfProfiler.detectRegressions(),
142
+ alerts: () => perfProfiler.getAlerts(),
143
+ thresholds: () => Object.fromEntries(perfProfiler._thresholds),
144
+ }, 'Performance Profiler');
145
+ }
146
+
147
+ results.profiler = true;
148
+ logger.info('Performance profiler initialized');
149
+ } catch (error) {
150
+ results.errors.push({ subsystem: 'profiler', error: error.message });
151
+ logger.error('Profiler bootstrap failed', { error: error.message });
152
+ }
153
+
154
+ try {
155
+ const exportTarget = options.exportTarget || process.env.OBSERVABILITY_EXPORT_TARGET;
156
+ if (exportTarget) {
157
+ await createExportSink({
158
+ target: exportTarget,
159
+ url: options.exportUrl || process.env.OBSERVABILITY_EXPORT_URL,
160
+ batchSize: options.batchSize,
161
+ batchIntervalMs: options.batchIntervalMs,
162
+ });
163
+ results.export = true;
164
+ logger.info('Export sink initialized', { target: exportTarget });
165
+ }
166
+ } catch (error) {
167
+ results.errors.push({ subsystem: 'export', error: error.message });
168
+ logger.error('Export sink bootstrap failed', { error: error.message });
169
+ }
170
+
171
+ try {
172
+ withHookTracing(hookEngine);
173
+ results.hookTracing = true;
174
+ logger.info('Hook tracing initialized');
175
+ } catch (error) {
176
+ results.errors.push({ subsystem: 'hookTracing', error: error.message });
177
+ logger.error('Hook tracing bootstrap failed', { error: error.message });
178
+ }
179
+
180
+ return results;
181
+ }
182
+
183
+ export async function shutdownObservability() {
184
+ const { getExportSink } = await import('./export-sink.js');
185
+ const sink = getExportSink();
186
+ if (sink) {
187
+ await sink.close();
188
+ }
189
+
190
+ tracer.clear();
191
+ perfProfiler.clear();
192
+
193
+ logger.info('Observability shutdown complete');
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Single init entry point (new — what the merge adds): runs both halves.
198
+ // ---------------------------------------------------------------------------
199
+ export async function initializeObservability(config = {}) {
200
+ const { monitoring = {}, bootstrap = {} } = config;
201
+
202
+ initializeMonitoring(monitoring);
203
+ const bootstrapResults = await bootstrapObservability(bootstrap);
204
+
205
+ return { monitoring: monitoringInitialized, bootstrap: bootstrapResults };
206
+ }
207
+
208
+ export async function shutdownObservabilityAll() {
209
+ await shutdownMonitoring();
210
+ await shutdownObservability();
211
+ }
212
+
213
+ export default initializeObservability;
@@ -0,0 +1,468 @@
1
+ // Merged from three parallel timing layers:
2
+ // - perf-profiler.js: the real, actively-used PerfProfiler class (record/
3
+ // measure/getStats/regressions/alerts), consumed by request-tracing.js,
4
+ // observability-bootstrap.js, and api/debug/[[...path]]/route.js.
5
+ // - perf-monitor.js: a simpler Map-based timer/threshold API
6
+ // (startTimer/endTimer/recordMetric/getStats/measure), zero external
7
+ // call sites at merge time, kept for its lighter-weight API shape.
8
+ // - query-perf.js: SQL-specific query tracking (trackQuery/getSlowQueries),
9
+ // zero external call sites at merge time.
10
+ // perf-monitor.js's and query-perf.js's `getMetrics`/`clearMetrics` collided
11
+ // on name; query-perf.js's versions are renamed getQueryMetrics/
12
+ // clearQueryMetrics below (nothing imported the unqualified names, confirmed
13
+ // by grep, so this is a safe rename with zero call sites to migrate).
14
+
15
+ import { createLogger } from './logger.js';
16
+
17
+ const logger = createLogger('[Perf]');
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // From perf-profiler.js — the real, actively-used API.
21
+ // ---------------------------------------------------------------------------
22
+ const _profiles = new Map();
23
+ const _alerts = [];
24
+ const MAX_ALERTS = 100;
25
+ const BASELINE_WINDOW_MS = 3600000;
26
+
27
+ const DEFAULT_THRESHOLDS = {
28
+ 'http.request': 500,
29
+ 'db.query': 100,
30
+ 'db.write': 200,
31
+ 'hook.execute': 50,
32
+ 'workflow.transition': 100,
33
+ 'formula.evaluate': 50,
34
+ 'auth.login': 300,
35
+ 'render.page': 200,
36
+ };
37
+
38
+ export class PerfProfiler {
39
+ constructor() {
40
+ this._profiles = _profiles;
41
+ this._thresholds = new Map(Object.entries(DEFAULT_THRESHOLDS));
42
+ }
43
+
44
+ setThreshold(operation, thresholdMs) {
45
+ this._thresholds.set(operation, thresholdMs);
46
+ }
47
+
48
+ getThreshold(operation) {
49
+ return this._thresholds.get(operation) || 100;
50
+ }
51
+
52
+ record(operation, durationMs, metadata = {}) {
53
+ let profile = _profiles.get(operation);
54
+ if (!profile) {
55
+ profile = {
56
+ operation,
57
+ count: 0,
58
+ totalMs: 0,
59
+ minMs: Infinity,
60
+ maxMs: 0,
61
+ durations: [],
62
+ slowCount: 0,
63
+ lastRecorded: null,
64
+ metadata: {},
65
+ };
66
+ _profiles.set(operation, profile);
67
+ }
68
+
69
+ profile.count++;
70
+ profile.totalMs += durationMs;
71
+ profile.minMs = Math.min(profile.minMs, durationMs);
72
+ profile.maxMs = Math.max(profile.maxMs, durationMs);
73
+ profile.lastRecorded = Date.now();
74
+
75
+ profile.durations.push({ durationMs, timestamp: Date.now(), ...metadata });
76
+ while (profile.durations.length > 10000) {
77
+ profile.durations.shift();
78
+ }
79
+
80
+ const threshold = this.getThreshold(operation);
81
+ if (durationMs > threshold) {
82
+ profile.slowCount++;
83
+ this._maybeAlert(operation, durationMs, threshold, metadata);
84
+ }
85
+
86
+ return profile;
87
+ }
88
+
89
+ async measure(operation, fn, metadata = {}) {
90
+ const start = performance.now();
91
+ try {
92
+ const result = await fn();
93
+ const duration = performance.now() - start;
94
+ this.record(operation, duration, metadata);
95
+ return result;
96
+ } catch (error) {
97
+ const duration = performance.now() - start;
98
+ this.record(operation, duration, { ...metadata, error: error.message });
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ measureSync(operation, fn, metadata = {}) {
104
+ const start = performance.now();
105
+ try {
106
+ const result = fn();
107
+ const duration = performance.now() - start;
108
+ this.record(operation, duration, metadata);
109
+ return result;
110
+ } catch (error) {
111
+ const duration = performance.now() - start;
112
+ this.record(operation, duration, { ...metadata, error: error.message });
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ getStats(operation) {
118
+ const profile = _profiles.get(operation);
119
+ if (!profile) return null;
120
+
121
+ const durations = profile.durations.map(d => d.durationMs).sort((a, b) => a - b);
122
+ const count = durations.length;
123
+
124
+ return {
125
+ operation: profile.operation,
126
+ count: profile.count,
127
+ totalMs: profile.totalMs,
128
+ avgMs: profile.count > 0 ? profile.totalMs / profile.count : 0,
129
+ minMs: profile.minMs === Infinity ? 0 : profile.minMs,
130
+ maxMs: profile.maxMs,
131
+ p50: this._percentile(durations, 50),
132
+ p95: this._percentile(durations, 95),
133
+ p99: this._percentile(durations, 99),
134
+ slowCount: profile.slowCount,
135
+ threshold: this.getThreshold(operation),
136
+ lastRecorded: profile.lastRecorded,
137
+ };
138
+ }
139
+
140
+ getAllStats() {
141
+ return Array.from(_profiles.keys()).map(key => this.getStats(key));
142
+ }
143
+
144
+ getSlowOperations() {
145
+ return this.getAllStats()
146
+ .filter(s => s.slowCount > 0)
147
+ .sort((a, b) => b.slowCount - a.slowCount);
148
+ }
149
+
150
+ detectRegressions(windowMs = 300000) {
151
+ const regressions = [];
152
+ const now = Date.now();
153
+
154
+ for (const [operation, profile] of _profiles.entries()) {
155
+ const recent = profile.durations.filter(d => now - d.timestamp < windowMs);
156
+ const older = profile.durations.filter(d => now - d.timestamp >= windowMs && now - d.timestamp < windowMs * 2);
157
+
158
+ if (recent.length < 10 || older.length < 10) continue;
159
+
160
+ const recentAvg = recent.reduce((sum, d) => sum + d.durationMs, 0) / recent.length;
161
+ const olderAvg = older.reduce((sum, d) => sum + d.durationMs, 0) / older.length;
162
+
163
+ if (olderAvg > 0 && recentAvg > olderAvg * 1.5) {
164
+ regressions.push({
165
+ operation,
166
+ recentAvg: Math.round(recentAvg * 100) / 100,
167
+ olderAvg: Math.round(olderAvg * 100) / 100,
168
+ regressionPercent: Math.round((recentAvg / olderAvg - 1) * 100),
169
+ sampleSize: recent.length,
170
+ });
171
+ }
172
+ }
173
+
174
+ return regressions;
175
+ }
176
+
177
+ getAlerts() {
178
+ return [..._alerts];
179
+ }
180
+
181
+ clearAlerts() {
182
+ _alerts.length = 0;
183
+ }
184
+
185
+ clear() {
186
+ _profiles.clear();
187
+ _alerts.length = 0;
188
+ }
189
+
190
+ _percentile(sorted, p) {
191
+ if (sorted.length === 0) return 0;
192
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
193
+ return sorted[Math.max(0, index)];
194
+ }
195
+
196
+ _maybeAlert(operation, durationMs, thresholdMs, metadata) {
197
+ const alertKey = `${operation}:slow`;
198
+ const existing = _alerts.find(a => a.key === alertKey && Date.now() - a.timestamp < 60000);
199
+ if (existing) return;
200
+
201
+ const alert = {
202
+ key: alertKey,
203
+ operation,
204
+ type: 'slow_operation',
205
+ durationMs: Math.round(durationMs * 100) / 100,
206
+ thresholdMs,
207
+ severity: durationMs > thresholdMs * 5 ? 'critical' : durationMs > thresholdMs * 2 ? 'warning' : 'info',
208
+ timestamp: Date.now(),
209
+ metadata,
210
+ };
211
+
212
+ _alerts.push(alert);
213
+ while (_alerts.length > MAX_ALERTS) {
214
+ _alerts.shift();
215
+ }
216
+
217
+ logger.warn('Slow operation detected', alert);
218
+ }
219
+ }
220
+
221
+ export const perfProfiler = new PerfProfiler();
222
+
223
+ export function createPerfProfiler() {
224
+ return new PerfProfiler();
225
+ }
226
+
227
+ export function getPerfProfiler() {
228
+ return perfProfiler;
229
+ }
230
+
231
+ export async function measurePerf(operation, fn, metadata = {}) {
232
+ return perfProfiler.measure(operation, fn, metadata);
233
+ }
234
+
235
+ export function measurePerfSync(operation, fn, metadata = {}) {
236
+ return perfProfiler.measureSync(operation, fn, metadata);
237
+ }
238
+
239
+ export default PerfProfiler;
240
+
241
+ if (globalThis.__debug__) {
242
+ globalThis.__debug__.expose('perf', {
243
+ stats: (op) => op ? perfProfiler.getStats(op) : perfProfiler.getAllStats(),
244
+ slow: () => perfProfiler.getSlowOperations(),
245
+ regressions: () => perfProfiler.detectRegressions(),
246
+ alerts: () => perfProfiler.getAlerts(),
247
+ thresholds: () => Object.fromEntries(perfProfiler._thresholds),
248
+ }, 'Performance Profiler');
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // From perf-monitor.js — a simpler Map-based timer/threshold API.
253
+ // ---------------------------------------------------------------------------
254
+ const metrics = new Map();
255
+ const thresholds = { render: 100, query: 50, api: 200, total: 500 };
256
+ let enabled = process.env.PERF_MONITOR !== 'false';
257
+
258
+ export function startTimer(key) {
259
+ if (!enabled) return null;
260
+ const start = process.hrtime.bigint();
261
+ return { key, start };
262
+ }
263
+
264
+ export function endTimer(timer) {
265
+ if (!enabled || !timer) return 0;
266
+ const end = process.hrtime.bigint();
267
+ const ms = Number(end - timer.start) / 1000000;
268
+ recordMetric(timer.key, ms);
269
+ return ms;
270
+ }
271
+
272
+ export function recordMetric(key, value) {
273
+ if (!enabled) return;
274
+ if (!metrics.has(key)) metrics.set(key, []);
275
+ const arr = metrics.get(key);
276
+ arr.push({ value, ts: Date.now() });
277
+ if (arr.length > 1000) arr.shift();
278
+ }
279
+
280
+ export function getMetrics(key) {
281
+ if (!key) return Object.fromEntries(metrics.entries());
282
+ return metrics.get(key) || [];
283
+ }
284
+
285
+ export function getSimpleStats(key) {
286
+ const data = metrics.get(key) || [];
287
+ if (data.length === 0) return null;
288
+ const values = data.map(d => d.value);
289
+ values.sort((a, b) => a - b);
290
+ return {
291
+ count: values.length,
292
+ min: values[0],
293
+ max: values[values.length - 1],
294
+ avg: values.reduce((a, b) => a + b, 0) / values.length,
295
+ p50: values[Math.floor(values.length * 0.5)],
296
+ p95: values[Math.floor(values.length * 0.95)],
297
+ p99: values[Math.floor(values.length * 0.99)]
298
+ };
299
+ }
300
+
301
+ export function checkThreshold(key, value) {
302
+ if (!enabled) return true;
303
+ const limit = thresholds[key];
304
+ if (limit && value > limit) {
305
+ logger.warn(`${key} exceeded threshold: ${value.toFixed(2)}ms > ${limit}ms`);
306
+ return false;
307
+ }
308
+ return true;
309
+ }
310
+
311
+ export function setThreshold(key, ms) {
312
+ thresholds[key] = ms;
313
+ }
314
+
315
+ export function clearMetrics() {
316
+ metrics.clear();
317
+ }
318
+
319
+ export function enable() {
320
+ enabled = true;
321
+ }
322
+
323
+ export function disable() {
324
+ enabled = false;
325
+ }
326
+
327
+ export function measure(key, fn) {
328
+ const timer = startTimer(key);
329
+ try {
330
+ const result = fn();
331
+ if (result instanceof Promise) {
332
+ return result.finally(() => {
333
+ const ms = endTimer(timer);
334
+ checkThreshold(key, ms);
335
+ });
336
+ }
337
+ const ms = endTimer(timer);
338
+ checkThreshold(key, ms);
339
+ return result;
340
+ } catch (err) {
341
+ endTimer(timer);
342
+ throw err;
343
+ }
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // From query-perf.js — SQL-specific query tracking. `getMetrics`/
348
+ // `clearMetrics` renamed to getQueryMetrics/clearQueryMetrics to avoid
349
+ // colliding with perf-monitor.js's same-named exports above (nothing
350
+ // imported the unqualified names; see file-header note).
351
+ // ---------------------------------------------------------------------------
352
+ const queryMetrics = new Map();
353
+ const slowQueries = [];
354
+ const MAX_SLOW_QUERIES = 100;
355
+ const SLOW_QUERY_THRESHOLD = 100;
356
+
357
+ export const trackQuery = (sql, duration, params = []) => {
358
+ const key = sql.substring(0, 200);
359
+
360
+ if (!queryMetrics.has(key)) {
361
+ queryMetrics.set(key, {
362
+ sql: key,
363
+ count: 0,
364
+ totalTime: 0,
365
+ minTime: Infinity,
366
+ maxTime: 0,
367
+ times: []
368
+ });
369
+ }
370
+
371
+ const metric = queryMetrics.get(key);
372
+ metric.count++;
373
+ metric.totalTime += duration;
374
+ metric.minTime = Math.min(metric.minTime, duration);
375
+ metric.maxTime = Math.max(metric.maxTime, duration);
376
+ metric.times.push(duration);
377
+
378
+ if (metric.times.length > 1000) metric.times.shift();
379
+
380
+ if (duration >= SLOW_QUERY_THRESHOLD) {
381
+ slowQueries.push({
382
+ sql: key,
383
+ duration,
384
+ params: params.slice(0, 10),
385
+ timestamp: Date.now()
386
+ });
387
+
388
+ if (slowQueries.length > MAX_SLOW_QUERIES) slowQueries.shift();
389
+ }
390
+ };
391
+
392
+ const percentile = (arr, p) => {
393
+ if (arr.length === 0) return 0;
394
+ const sorted = [...arr].sort((a, b) => a - b);
395
+ const index = Math.ceil(sorted.length * p / 100) - 1;
396
+ return sorted[Math.max(0, index)];
397
+ };
398
+
399
+ export const getQueryMetrics = () => {
400
+ const result = [];
401
+
402
+ for (const [, metric] of queryMetrics) {
403
+ const avgTime = metric.count > 0 ? metric.totalTime / metric.count : 0;
404
+ const p50 = percentile(metric.times, 50);
405
+ const p95 = percentile(metric.times, 95);
406
+ const p99 = percentile(metric.times, 99);
407
+
408
+ result.push({
409
+ sql: metric.sql,
410
+ count: metric.count,
411
+ avgTime: avgTime.toFixed(2),
412
+ minTime: metric.minTime === Infinity ? 0 : metric.minTime.toFixed(2),
413
+ maxTime: metric.maxTime.toFixed(2),
414
+ p50: p50.toFixed(2),
415
+ p95: p95.toFixed(2),
416
+ p99: p99.toFixed(2),
417
+ totalTime: metric.totalTime.toFixed(2)
418
+ });
419
+ }
420
+
421
+ return result.sort((a, b) => parseFloat(b.totalTime) - parseFloat(a.totalTime));
422
+ };
423
+
424
+ export const getSlowQueries = () => {
425
+ return slowQueries.slice().reverse();
426
+ };
427
+
428
+ export const getSummary = () => {
429
+ const qm = getQueryMetrics();
430
+ const totalQueries = qm.reduce((sum, m) => sum + m.count, 0);
431
+ const totalTime = qm.reduce((sum, m) => sum + parseFloat(m.totalTime), 0);
432
+ const allTimes = [];
433
+
434
+ for (const [, metric] of queryMetrics) {
435
+ allTimes.push(...metric.times);
436
+ }
437
+
438
+ return {
439
+ totalQueries,
440
+ uniqueQueries: qm.length,
441
+ totalTime: totalTime.toFixed(2),
442
+ avgTime: totalQueries > 0 ? (totalTime / totalQueries).toFixed(2) : '0',
443
+ p50: percentile(allTimes, 50).toFixed(2),
444
+ p95: percentile(allTimes, 95).toFixed(2),
445
+ p99: percentile(allTimes, 99).toFixed(2),
446
+ slowQueries: slowQueries.length,
447
+ slowQueryThreshold: SLOW_QUERY_THRESHOLD
448
+ };
449
+ };
450
+
451
+ export const clearQueryMetrics = () => {
452
+ queryMetrics.clear();
453
+ slowQueries.length = 0;
454
+ };
455
+
456
+ export const withPerfTracking = (db, sql, params, executor) => {
457
+ const start = performance.now();
458
+ try {
459
+ const result = executor();
460
+ const duration = performance.now() - start;
461
+ trackQuery(sql, duration, params);
462
+ return result;
463
+ } catch (e) {
464
+ const duration = performance.now() - start;
465
+ trackQuery(sql, duration, params);
466
+ throw e;
467
+ }
468
+ };