nucleus-core-ts 0.9.946 → 0.9.948

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.build-ok CHANGED
@@ -1 +1 @@
1
- 0.9.946
1
+ 0.9.948
package/dist/index.js CHANGED
@@ -71,7 +71,7 @@ and limitations under the License.
71
71
  </div>
72
72
  </body>
73
73
  </html>
74
- `}getActiveAlerts(){return Array.from(this.state.activeAlerts.values())}acknowledgeAlert(alertId){for(let[_type,alert]of this.state.activeAlerts)if(alert.id===alertId)return alert.acknowledged=!0,!0;return!1}clearAlert(type){this.state.activeAlerts.delete(type)}}var init_AlertService=()=>{};class ApplicationCollector{config;requestCount=0;responseTimes=[];errorCount=0;rateLimitBlocks=0;byEndpoint={};byMethod={};byStatus={};byErrorType={};lastCollectTime=Date.now();constructor(config){this.config=config}recordRequest(params){if(!this.config?.enabled)return;if(this.requestCount++,this.config.metrics?.responseTime!==!1){if(this.responseTimes.push(params.responseTimeMs),this.responseTimes.length>1e4)this.responseTimes=this.responseTimes.slice(-5000)}if(this.config.metrics?.requests!==!1)this.byEndpoint[params.endpoint]=(this.byEndpoint[params.endpoint]||0)+1,this.byMethod[params.method]=(this.byMethod[params.method]||0)+1,this.byStatus[String(params.status)]=(this.byStatus[String(params.status)]||0)+1;if(this.config.metrics?.errors!==!1&&params.isError){if(this.errorCount++,params.errorType)this.byErrorType[params.errorType]=(this.byErrorType[params.errorType]||0)+1}}recordRateLimitBlock(){if(!this.config?.enabled||this.config.metrics?.rateLimits===!1)return;this.rateLimitBlocks++}collect(){if(!this.config?.enabled)return null;let elapsed=(Date.now()-this.lastCollectTime)/1000/60,perMinute=elapsed>0?Math.round(this.requestCount/elapsed):0,blockedPerMinute=elapsed>0?Math.round(this.rateLimitBlocks/elapsed):0,sortedTimes=[...this.responseTimes].sort((a,b)=>a-b),len=sortedTimes.length;return{requests:{total:this.requestCount,perMinute,byEndpoint:{...this.byEndpoint},byMethod:{...this.byMethod},byStatus:{...this.byStatus}},responseTime:{avg:len>0?Math.round(sortedTimes.reduce((a,b)=>a+b,0)/len*100)/100:0,min:len>0?sortedTimes[0]??0:0,max:len>0?sortedTimes[len-1]??0:0,p50:len>0?sortedTimes[Math.floor(len*0.5)]??0:0,p95:len>0?sortedTimes[Math.floor(len*0.95)]??0:0,p99:len>0?sortedTimes[Math.floor(len*0.99)]??0:0},errors:{total:this.errorCount,rate:this.requestCount>0?Math.round(this.errorCount/this.requestCount*100*100)/100:0,byType:{...this.byErrorType}},rateLimits:{blocked:this.rateLimitBlocks,blockedPerMinute}}}reset(){this.requestCount=0,this.responseTimes=[],this.errorCount=0,this.rateLimitBlocks=0,this.byEndpoint={},this.byMethod={},this.byStatus={},this.byErrorType={},this.lastCollectTime=Date.now()}getRequestsPerMinute(){let elapsed=(Date.now()-this.lastCollectTime)/1000/60;return elapsed>0?Math.round(this.requestCount/elapsed):0}getErrorRate(){return this.requestCount>0?this.errorCount/this.requestCount*100:0}getRateLimitBlocksPerMinute(){let elapsed=(Date.now()-this.lastCollectTime)/1000/60;return elapsed>0?Math.round(this.rateLimitBlocks/elapsed):0}getAvgResponseTime(){if(this.responseTimes.length===0)return 0;return this.responseTimes.reduce((a,b)=>a+b,0)/this.responseTimes.length}}class DatabaseCollector{config;execute;lastTotals=null;activeTimeSupported=!0;constructor(config,execute){this.config=config,this.execute=execute}async collect(){if(!this.config.enabled)return null;let metrics={connections:{active:0,idle:0,total:0},queries:{total:0,avgTime:0,slowQueries:0}};if(this.config.metrics?.connections!==!1)metrics.connections=await this.collectConnections();if(this.config.metrics?.queryTime!==!1)metrics.queries=await this.collectQueries();return metrics}toNumber(value){let num=Number.parseFloat(String(value??0));return Number.isNaN(num)?0:num}async collectConnections(){try{let row=(await this.execute(`SELECT
74
+ `}getActiveAlerts(){return Array.from(this.state.activeAlerts.values())}acknowledgeAlert(alertId){for(let[_type,alert]of this.state.activeAlerts)if(alert.id===alertId)return alert.acknowledged=!0,!0;return!1}clearAlert(type){this.state.activeAlerts.delete(type)}}var init_AlertService=()=>{};class ApplicationCollector{config;requestCount=0;responseTimes=[];errorCount=0;rateLimitBlocks=0;byEndpoint={};byMethod={};byStatus={};byErrorType={};lastCollectTime=Date.now();buckets=[];constructor(config){this.config=config}bucketFor(second){let index=second%60,bucket=this.buckets[index];if(!bucket)return bucket={second,requests:0,errors:0,blocks:0},this.buckets[index]=bucket,bucket;if(bucket.second!==second)bucket.second=second,bucket.requests=0,bucket.errors=0,bucket.blocks=0;return bucket}windowed(pick){let oldest=Math.floor(Date.now()/1000)-60,total=0;for(let bucket of this.buckets)if(bucket&&bucket.second>oldest)total+=pick(bucket);return total}recordRequest(params){if(!this.config?.enabled)return;this.requestCount++;let bucket=this.bucketFor(Math.floor(Date.now()/1000));if(bucket.requests+=1,params.isError)bucket.errors+=1;if(this.config.metrics?.responseTime!==!1){if(this.responseTimes.push({at:Date.now(),ms:params.responseTimeMs}),this.responseTimes.length>50000)this.pruneResponseTimes()}if(this.config.metrics?.requests!==!1)this.byEndpoint[params.endpoint]=(this.byEndpoint[params.endpoint]||0)+1,this.byMethod[params.method]=(this.byMethod[params.method]||0)+1,this.byStatus[String(params.status)]=(this.byStatus[String(params.status)]||0)+1;if(this.config.metrics?.errors!==!1&&params.isError){if(this.errorCount++,params.errorType)this.byErrorType[params.errorType]=(this.byErrorType[params.errorType]||0)+1}}recordRateLimitBlock(){if(!this.config?.enabled||this.config.metrics?.rateLimits===!1)return;this.rateLimitBlocks++,this.bucketFor(Math.floor(Date.now()/1000)).blocks+=1}pruneResponseTimes(){let oldest=Date.now()-60000,kept=this.responseTimes.filter((sample)=>sample.at>oldest);if(kept.length>50000)kept=kept.slice(-50000);this.responseTimes=kept}collect(){if(!this.config?.enabled)return null;let perMinute=this.windowed((b)=>b.requests),blockedPerMinute=this.windowed((b)=>b.blocks);this.pruneResponseTimes();let sortedTimes=this.responseTimes.map((s)=>s.ms).sort((a,b)=>a-b),len=sortedTimes.length;return{requests:{total:this.requestCount,perMinute,byEndpoint:{...this.byEndpoint},byMethod:{...this.byMethod},byStatus:{...this.byStatus}},responseTime:{avg:len>0?Math.round(sortedTimes.reduce((a,b)=>a+b,0)/len*100)/100:0,min:len>0?sortedTimes[0]??0:0,max:len>0?sortedTimes[len-1]??0:0,p50:len>0?sortedTimes[Math.floor(len*0.5)]??0:0,p95:len>0?sortedTimes[Math.floor(len*0.95)]??0:0,p99:len>0?sortedTimes[Math.floor(len*0.99)]??0:0},errors:{total:this.errorCount,rate:(()=>{let requests=perMinute;if(requests===0)return 0;let errors=this.windowed((b)=>b.errors);return Math.round(errors/requests*100*100)/100})(),byType:{...this.byErrorType}},rateLimits:{blocked:this.rateLimitBlocks,blockedPerMinute}}}reset(){this.requestCount=0,this.responseTimes=[],this.errorCount=0,this.rateLimitBlocks=0,this.byEndpoint={},this.byMethod={},this.byStatus={},this.byErrorType={},this.buckets=[],this.lastCollectTime=Date.now()}getRequestsPerMinute(){return this.windowed((b)=>b.requests)}getErrorRate(){let requests=this.windowed((b)=>b.requests);return requests>0?this.windowed((b)=>b.errors)/requests*100:0}getRateLimitBlocksPerMinute(){return this.windowed((b)=>b.blocks)}getAvgResponseTime(){if(this.pruneResponseTimes(),this.responseTimes.length===0)return 0;return this.responseTimes.reduce((total,s)=>total+s.ms,0)/this.responseTimes.length}}class DatabaseCollector{config;execute;lastTotals=null;activeTimeSupported=!0;constructor(config,execute){this.config=config,this.execute=execute}async collect(){if(!this.config.enabled)return null;let metrics={connections:{active:0,idle:0,total:0},queries:{total:0,avgTime:0,slowQueries:0}};if(this.config.metrics?.connections!==!1)metrics.connections=await this.collectConnections();if(this.config.metrics?.queryTime!==!1)metrics.queries=await this.collectQueries();return metrics}toNumber(value){let num=Number.parseFloat(String(value??0));return Number.isNaN(num)?0:num}async collectConnections(){try{let row=(await this.execute(`SELECT
75
75
  count(*) FILTER (WHERE state = 'active') AS active,
76
76
  count(*) FILTER (WHERE state = 'idle') AS idle,
77
77
  count(*) AS total
@@ -2,6 +2,16 @@ import type { ApplicationMetrics, MonitoringConfig } from '../types';
2
2
  export declare class ApplicationCollector {
3
3
  private config;
4
4
  private requestCount;
5
+ /**
6
+ * Response times with the moment they happened, so percentiles describe NOW.
7
+ *
8
+ * They used to be a plain list of the last 5–10.000 numbers. On a quiet
9
+ * install that is not a window, it is a memory: run a load test at lunchtime
10
+ * and the p95 on the screen still describes the load test at six in the
11
+ * evening, because nothing has come along to push those samples out. The
12
+ * peak recorder then writes that stale p95 into a peak's context as "what
13
+ * the response time was at the time", which it was not.
14
+ */
5
15
  private responseTimes;
6
16
  private errorCount;
7
17
  private rateLimitBlocks;
@@ -10,7 +20,34 @@ export declare class ApplicationCollector {
10
20
  private byStatus;
11
21
  private byErrorType;
12
22
  private lastCollectTime;
23
+ /**
24
+ * The last sixty seconds, one bucket each.
25
+ *
26
+ * ## What this replaces, and why it is not a detail
27
+ *
28
+ * `perMinute` used to be `requestCount / minutesSinceBoot` — the lifetime
29
+ * AVERAGE, presented as the current rate, on a chart labelled "requests per
30
+ * minute" and in the alert thresholds. `reset()` exists to make that
31
+ * arithmetic correct and is called from nowhere, so the divisor kept growing
32
+ * for as long as the pod lived.
33
+ *
34
+ * What that does to a real number: measured on 6 August 2026, a load run
35
+ * driving roughly fifty API requests a SECOND was recorded as a peak of 273
36
+ * per MINUTE, in the same snapshot that reported 370 cumulative hits on the
37
+ * busiest endpoint alone — two figures that cannot both be true. A pod up for
38
+ * a week would have flattened a genuine incident into nothing at all, which
39
+ * is the same failure in the direction that matters: the alert that never
40
+ * fires.
41
+ *
42
+ * A rolling window costs sixty small objects and answers the question the
43
+ * metric's name promises.
44
+ */
45
+ private buckets;
13
46
  constructor(config: MonitoringConfig['application']);
47
+ /** The bucket for `now`, recycled if this slot belongs to an expired second. */
48
+ private bucketFor;
49
+ /** Sums the buckets still inside the window. */
50
+ private windowed;
14
51
  recordRequest(params: {
15
52
  endpoint: string;
16
53
  method: string;
@@ -20,10 +57,26 @@ export declare class ApplicationCollector {
20
57
  errorType?: string;
21
58
  }): void;
22
59
  recordRateLimitBlock(): void;
60
+ /** Drops samples that have fallen out of the window, then out of the cap. */
61
+ private pruneResponseTimes;
23
62
  collect(): ApplicationMetrics | null;
63
+ /**
64
+ * Clears the cumulative counters.
65
+ *
66
+ * Kept for callers that want a fresh baseline, but nothing on the boot path
67
+ * calls it — and that used to be a silent bug rather than a choice, because
68
+ * the rate arithmetic depended on it. The rates no longer do: they read the
69
+ * rolling window, which expires on its own.
70
+ */
24
71
  reset(): void;
72
+ /** Requests in the last sixty seconds. */
25
73
  getRequestsPerMinute(): number;
74
+ /**
75
+ * Error rate over the last minute — the same window the alert thresholds
76
+ * should be judging, and the same one `collect()` reports.
77
+ */
26
78
  getErrorRate(): number;
27
79
  getRateLimitBlocksPerMinute(): number;
80
+ /** Mean response time over the same minute the other rates describe. */
28
81
  getAvgResponseTime(): number;
29
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.946",
3
+ "version": "0.9.948",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",