Skip to content

Commit 72f246e

Browse files
committed
feat: initial release v0.1.0 — Value tracking with threshold crossing detection and time-in-range stats
0 parents  commit 72f246e

10 files changed

Lines changed: 483 additions & 0 deletions

File tree

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
dist/
3+
.env
4+
.env.local
5+
*.log
6+
.DS_Store
7+
Thumbs.db
8+
coverage/
9+
.turbo/
10+
*.tsbuildinfo

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 AdametherzLab
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# threshold-history 📈🚨💡
2+
3+
**Threshold-aware value tracking with smart alerts & time-in-range stats**
4+
## 🚀 Why Use This?
5+
6+
Track values over time, get alerted when thresholds are crossed, and calculate how often values stay in desired ranges. Like a Fitbit for your IoT devices! 🔋📡
7+
8+
## 📦 Installation
9+
10+
```bash
11+
# Using Bun (recommended)
12+
bun add threshold-history
13+
14+
# Using npm
15+
npm install threshold-history
16+
```
17+
18+
## 💻 Quick Start
19+
20+
```typescript
21+
// REMOVED external import: import { ValueTracker } from 'threshold-history';
22+
23+
// Monitor room temperature between 18°C-25°C
24+
const tracker = new ValueTracker({
25+
thresholds: [
26+
{ value: 18, direction: 'above', label: 'too-cold' },
27+
{ value: 25, direction: 'below', label: 'too-hot' }
28+
],
29+
bufferSize: 1000
30+
});
31+
32+
// Add temperature readings
33+
tracker.addValue(22, Date.now() - 3600_000); // 1 hour ago
34+
tracker.addValue(17.5); // Now - crosses lower threshold!
35+
36+
// Get critical events
37+
console.log(tracker.getCrossings());
38+
// Returns: [{ timestamp: 1717045200000, threshold: 'too-cold', direction: 'below' }]
39+
40+
// Calculate time-in-range
41+
console.log(tracker.getStats().inRangePercentage);
42+
// Returns: 97.2 (percent of time in acceptable range)
43+
```
44+
45+
## 📖 Core API
46+
47+
### `ValueTracker(config)`
48+
- `config.thresholds`: Array of `ThresholdConfig`
49+
```typescript
50+
type ThresholdConfig = {
51+
value: number;
52+
direction: 'above' | 'below';
53+
label: string;
54+
};
55+
```
56+
- `config.bufferSize`: Max stored entries (default: 1000)
57+
58+
### Key Methods
59+
- **`.addValue(value: number, timestamp = Date.now())`**
60+
Record new measurement
61+
- **`.getCrossings(): ThresholdCrossing[]`**
62+
Returns threshold crossings with timestamps
63+
- **`.getStats(): TrackingStats`**
64+
Returns time-in-range %, total duration, and value statistics
65+
66+
## 🧪 Real-World Example
67+
68+
```typescript
69+
// Monitor server CPU temperature
70+
const serverMonitor = new ValueTracker({
71+
thresholds: [
72+
{ value: 70, direction: 'above', label: 'overheat-warning' },
73+
{ value: 90, direction: 'above', label: 'critical-shutdown' }
74+
]
75+
});
76+
77+
// Simulate temperature spikes
78+
serverMonitor.addValue(65);
79+
serverMonitor.addValue(72); // Triggers warning
80+
serverMonitor.addValue(85);
81+
serverMonitor.addValue(91); // Critical shutdown!
82+
83+
console.log(serverMonitor.getStats());
84+
// {
85+
// inRangePercentage: 66.6,
86+
// totalDuration: 3600000,
87+
// valueStats: { min: 65, max: 91, avg: 78.25 }
88+
// }
89+
```
90+
91+
## 🤝 Contributing
92+
93+
Found a bug? Got a cool idea?
94+
1. Fork it 🍴
95+
2. Code it 💻
96+
3. PR it 🚀
97+
We <3 quality contributions!
98+
99+
## 📄 License
100+
101+
MIT © [AdametherzLab](https://github.com/AdametherzLab)
102+
Made with ❤️ and enough caffeine to power a small city

SECURITY.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Security Policy
2+
3+
## Reporting a Vulnerability
4+
5+
If you discover a security vulnerability in this project, please report it responsibly.
6+
7+
**Contact:** security@adametherzlab.com
8+
9+
**Process:**
10+
1. Email the details of the vulnerability
11+
2. Include steps to reproduce if possible
12+
3. We will acknowledge your report within 48 hours
13+
4. We aim to fix critical issues within 7 days
14+
15+
## Supported Versions
16+
17+
| Version | Supported |
18+
| ------- | --------- |
19+
| 0.x.x | Yes |
20+
21+
## Disclosure Policy
22+
23+
- We follow responsible disclosure practices
24+
- Please do not publicly disclose vulnerabilities before they are fixed
25+
- We will credit reporters in our release notes (unless you prefer anonymity)

package.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@adametherzlab/threshold-history",
3+
"version": "0.1.0",
4+
"description": "Value tracking with threshold crossing detection and time-in-range stats",
5+
"type": "module",
6+
"main": "src/index.ts",
7+
"module": "src/index.ts",
8+
"types": "src/index.ts",
9+
"exports": {
10+
".": {
11+
"types": "./src/index.ts",
12+
"import": "./src/index.ts",
13+
"default": "./src/index.ts"
14+
}
15+
},
16+
"scripts": {
17+
"test": "bun test",
18+
"build": "bun build src/index.ts --outdir dist",
19+
"typecheck": "tsc --noEmit"
20+
},
21+
"devDependencies": {
22+
"bun-types": "latest",
23+
"typescript": "^5.5.0"
24+
},
25+
"keywords": [
26+
"typescript",
27+
"bun",
28+
"monitoring",
29+
"thresholds",
30+
"time-series",
31+
"condition-monitoring"
32+
],
33+
"author": "AdametherzLab",
34+
"license": "MIT",
35+
"repository": {
36+
"type": "git",
37+
"url": "https://github.com/AdametherzLab/threshold-history.git"
38+
},
39+
"homepage": "https://github.com/AdametherzLab/threshold-history#readme",
40+
"bugs": {
41+
"url": "https://github.com/AdametherzLab/threshold-history/issues"
42+
},
43+
"engines": {
44+
"node": ">=18"
45+
},
46+
"sideEffects": false
47+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { ThresholdConfig, ThresholdCrossing, TrackerConfig, TrackingState, TrackingStats } from './types';
2+
export { ValueTracker } from './tracker';

src/tracker.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { TrackerConfig, TrackingState, ThresholdCrossing, TrackingStats } from './types';
2+
3+
export class ValueTracker {
4+
private readonly config: TrackerConfig;
5+
private history: Array<{ value: number; timestamp: number; state: 'within' | 'above' | 'below' }>;
6+
private currentIndex: number;
7+
private currentState: 'within' | 'above' | 'below' | null;
8+
private crossings: ThresholdCrossing[];
9+
private stats: { timeInRange: number; totalCrossings: number; lastCrossing: Date | null; currentStatus: 'within' | 'above' | 'below' };
10+
private lastTimestamp: number | null;
11+
12+
constructor(config: TrackerConfig) {
13+
this.config = config;
14+
this.history = [];
15+
this.currentIndex = 0;
16+
this.currentState = null;
17+
this.crossings = [];
18+
this.stats = { timeInRange: 0, totalCrossings: 0, lastCrossing: null, currentStatus: 'within' };
19+
this.lastTimestamp = null;
20+
}
21+
22+
processValue(value: number): void {
23+
const precision = this.config.valuePrecisionDigits;
24+
const roundedValue = Number(value.toFixed(precision));
25+
const timestamp = Date.now();
26+
const newState = this.calculateNewState(roundedValue);
27+
28+
if (this.currentState !== null && newState !== this.currentState) {
29+
const crossing = this.determineCrossing(this.currentState, newState);
30+
this.crossings.push({
31+
timestamp: new Date(timestamp),
32+
value: roundedValue,
33+
direction: crossing.direction,
34+
});
35+
this.stats.totalCrossings++;
36+
this.stats.lastCrossing = new Date(timestamp);
37+
}
38+
39+
if (this.lastTimestamp !== null && this.currentState !== null) {
40+
const delta = timestamp - this.lastTimestamp;
41+
if (this.currentState === 'within') {
42+
this.stats.timeInRange += delta;
43+
}
44+
}
45+
46+
this.currentState = newState;
47+
this.stats.currentStatus = newState;
48+
this.lastTimestamp = timestamp;
49+
50+
const entry = { value: roundedValue, timestamp, state: newState };
51+
const maxHistory = Math.floor(this.config.samplingIntervalMs > 0 ? 1000 : 100);
52+
if (this.history.length < maxHistory) {
53+
this.history.push(entry);
54+
} else {
55+
this.history[this.currentIndex] = entry;
56+
this.currentIndex = (this.currentIndex + 1) % maxHistory;
57+
}
58+
}
59+
60+
getStats(): TrackingStats {
61+
const totalTime = this.lastTimestamp !== null && this.history.length > 0
62+
? this.lastTimestamp - this.history[0].timestamp
63+
: 0;
64+
const timeInRange = totalTime > 0 ? this.stats.timeInRange : 0;
65+
return {
66+
timeInRange,
67+
lastCrossing: this.stats.lastCrossing ?? undefined,
68+
totalCrossings: this.stats.totalCrossings,
69+
currentStatus: this.stats.currentStatus
70+
};
71+
}
72+
73+
getHistory(): Array<{ value: number; timestamp: number; state: 'within' | 'above' | 'below' }> {
74+
return [...this.history];
75+
}
76+
77+
getCrossings(): ThresholdCrossing[] {
78+
return [...this.crossings];
79+
}
80+
81+
reset(): void {
82+
this.history = [];
83+
this.currentIndex = 0;
84+
this.currentState = null;
85+
this.crossings = [];
86+
this.stats = { timeInRange: 0, totalCrossings: 0, lastCrossing: null, currentStatus: 'within' };
87+
this.lastTimestamp = null;
88+
}
89+
90+
private calculateNewState(value: number): 'within' | 'above' | 'below' {
91+
if (this.currentState === null) {
92+
return this.determineInitialState(value);
93+
}
94+
95+
const { lower, upper, lowerHysteresis = 0, upperHysteresis = 0 } = this.config.thresholds;
96+
let adjustedLower = lower;
97+
let adjustedUpper = upper;
98+
99+
switch (this.currentState) {
100+
case 'above': adjustedUpper -= upperHysteresis; break;
101+
case 'below': adjustedLower += lowerHysteresis; break;
102+
}
103+
104+
if (value > adjustedUpper) return 'above';
105+
if (value < adjustedLower) return 'below';
106+
return 'within';
107+
}
108+
109+
private determineInitialState(value: number): 'within' | 'above' | 'below' {
110+
const { lower, upper } = this.config.thresholds;
111+
if (value > upper) return 'above';
112+
if (value < lower) return 'below';
113+
return 'within';
114+
}
115+
116+
private determineCrossing(
117+
previousState: 'within' | 'above' | 'below',
118+
newState: 'within' | 'above' | 'below'
119+
): { direction: 'above' | 'below' } {
120+
if (previousState === 'within' && newState === 'above') return { direction: 'above' };
121+
if (previousState === 'above' && newState === 'within') return { direction: 'below' };
122+
if (previousState === 'within' && newState === 'below') return { direction: 'below' };
123+
if (previousState === 'below' && newState === 'within') return { direction: 'above' };
124+
if (previousState === 'above' && newState === 'below') return { direction: 'below' };
125+
if (previousState === 'below' && newState === 'above') return { direction: 'above' };
126+
throw new Error(`Unexpected state transition: ${previousState}${newState}`);
127+
}
128+
}

src/types.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
interface ThresholdConfig {
3+
readonly lower: number;
4+
readonly upper: number;
5+
readonly lowerHysteresis?: number;
6+
readonly upperHysteresis?: number;
7+
}
8+
9+
interface ThresholdCrossing {
10+
readonly timestamp: Date;
11+
readonly direction: 'above' | 'below';
12+
readonly value: number;
13+
}
14+
15+
interface TrackingStats {
16+
timeInRange: number;
17+
lastCrossing?: Date;
18+
totalCrossings: number;
19+
currentStatus: 'within' | 'above' | 'below';
20+
}
21+
22+
interface TrackingState {
23+
currentValue: number;
24+
history: ThresholdCrossing[];
25+
stats: TrackingStats;
26+
}
27+
28+
interface TrackerConfig {
29+
readonly samplingIntervalMs: number;
30+
readonly valuePrecisionDigits: number;
31+
readonly thresholds: ThresholdConfig;
32+
}
33+
34+
export type {
35+
ThresholdConfig,
36+
ThresholdCrossing,
37+
TrackingStats,
38+
TrackingState,
39+
TrackerConfig
40+
};

0 commit comments

Comments
 (0)