homebridge-myleviton 3.0.0
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/LICENSE +202 -0
- package/README.md +112 -0
- package/config.schema.json +136 -0
- package/dist/api/cache.d.ts +108 -0
- package/dist/api/cache.d.ts.map +1 -0
- package/dist/api/cache.js +206 -0
- package/dist/api/cache.js.map +1 -0
- package/dist/api/circuit-breaker.d.ts +118 -0
- package/dist/api/circuit-breaker.d.ts.map +1 -0
- package/dist/api/circuit-breaker.js +223 -0
- package/dist/api/circuit-breaker.js.map +1 -0
- package/dist/api/client.d.ts +116 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +358 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/index.d.ts +23 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +47 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/persistence.d.ts +107 -0
- package/dist/api/persistence.d.ts.map +1 -0
- package/dist/api/persistence.js +285 -0
- package/dist/api/persistence.js.map +1 -0
- package/dist/api/rate-limiter.d.ts +102 -0
- package/dist/api/rate-limiter.d.ts.map +1 -0
- package/dist/api/rate-limiter.js +173 -0
- package/dist/api/rate-limiter.js.map +1 -0
- package/dist/api/request-queue.d.ts +104 -0
- package/dist/api/request-queue.d.ts.map +1 -0
- package/dist/api/request-queue.js +223 -0
- package/dist/api/request-queue.js.map +1 -0
- package/dist/api/websocket.d.ts +116 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +319 -0
- package/dist/api/websocket.js.map +1 -0
- package/dist/errors/index.d.ts +182 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +273 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +139 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +664 -0
- package/dist/platform.js.map +1 -0
- package/dist/types/index.d.ts +225 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +34 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +52 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +103 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +184 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +56 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +141 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/sanitizers.d.ts +37 -0
- package/dist/utils/sanitizers.d.ts.map +1 -0
- package/dist/utils/sanitizers.js +128 -0
- package/dist/utils/sanitizers.js.map +1 -0
- package/dist/utils/validators.d.ts +51 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +243 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 tbaur
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
* See LICENSE file for full license text
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Device state persistence for faster startup and offline resilience
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.DevicePersistence = exports.PERSISTENCE_FILE_NAME = exports.DEFAULT_PERSISTENCE_CONFIG = void 0;
|
|
45
|
+
exports.getDevicePersistence = getDevicePersistence;
|
|
46
|
+
exports.resetGlobalPersistence = resetGlobalPersistence;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
/**
|
|
50
|
+
* Default persistence configuration
|
|
51
|
+
*/
|
|
52
|
+
exports.DEFAULT_PERSISTENCE_CONFIG = {
|
|
53
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
54
|
+
maxDevices: 200,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Default persistence file name
|
|
58
|
+
*/
|
|
59
|
+
exports.PERSISTENCE_FILE_NAME = '.homebridge-myleviton-state.json';
|
|
60
|
+
/**
|
|
61
|
+
* Device state persistence manager
|
|
62
|
+
* Stores device states for faster startup and offline resilience
|
|
63
|
+
*/
|
|
64
|
+
class DevicePersistence {
|
|
65
|
+
storagePath;
|
|
66
|
+
maxAge;
|
|
67
|
+
maxDevices;
|
|
68
|
+
deviceStates = new Map();
|
|
69
|
+
loaded = false;
|
|
70
|
+
dirty = false;
|
|
71
|
+
constructor(storagePath, config = {}) {
|
|
72
|
+
const merged = { ...exports.DEFAULT_PERSISTENCE_CONFIG, ...config };
|
|
73
|
+
this.storagePath = storagePath || path.join(process.env.HOME || '/tmp', exports.PERSISTENCE_FILE_NAME);
|
|
74
|
+
this.maxAge = merged.maxAge ?? 24 * 60 * 60 * 1000;
|
|
75
|
+
this.maxDevices = merged.maxDevices ?? 200;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load persisted device states from disk
|
|
79
|
+
*/
|
|
80
|
+
load() {
|
|
81
|
+
if (this.loaded) {
|
|
82
|
+
return this.deviceStates;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(this.storagePath)) {
|
|
86
|
+
const data = fs.readFileSync(this.storagePath, 'utf8');
|
|
87
|
+
const parsed = JSON.parse(data);
|
|
88
|
+
// Validate structure
|
|
89
|
+
if (parsed && typeof parsed === 'object' && parsed.devices) {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const maxAge = this.maxAge;
|
|
92
|
+
for (const [id, state] of Object.entries(parsed.devices)) {
|
|
93
|
+
if (state && typeof state === 'object') {
|
|
94
|
+
// Check if data is too old
|
|
95
|
+
const cachedAt = parsed.timestamp || 0;
|
|
96
|
+
if (now - cachedAt < maxAge) {
|
|
97
|
+
this.deviceStates.set(id, {
|
|
98
|
+
...state,
|
|
99
|
+
_cached: true,
|
|
100
|
+
_cachedAt: cachedAt,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore load errors - start fresh
|
|
110
|
+
this.deviceStates.clear();
|
|
111
|
+
}
|
|
112
|
+
this.loaded = true;
|
|
113
|
+
return this.deviceStates;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Save device states to disk
|
|
117
|
+
*/
|
|
118
|
+
save() {
|
|
119
|
+
if (!this.dirty && this.loaded) {
|
|
120
|
+
return true; // Nothing to save
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// Clean up internal properties before saving
|
|
124
|
+
const devices = {};
|
|
125
|
+
// Limit size
|
|
126
|
+
let count = 0;
|
|
127
|
+
for (const [id, state] of this.deviceStates.entries()) {
|
|
128
|
+
if (count >= this.maxDevices) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
const { _cached, _cachedAt, _updatedAt, ...cleanState } = state;
|
|
132
|
+
devices[id] = cleanState;
|
|
133
|
+
count++;
|
|
134
|
+
}
|
|
135
|
+
const data = JSON.stringify({
|
|
136
|
+
version: 1,
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
devices,
|
|
139
|
+
}, null, 2);
|
|
140
|
+
// Write atomically using temp file
|
|
141
|
+
const tempPath = `${this.storagePath}.tmp`;
|
|
142
|
+
fs.writeFileSync(tempPath, data, 'utf8');
|
|
143
|
+
fs.renameSync(tempPath, this.storagePath);
|
|
144
|
+
this.dirty = false;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Update state for a device
|
|
153
|
+
*/
|
|
154
|
+
updateDevice(deviceId, state) {
|
|
155
|
+
if (!deviceId) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const existing = this.deviceStates.get(deviceId) || { id: deviceId };
|
|
159
|
+
this.deviceStates.set(deviceId, {
|
|
160
|
+
...existing,
|
|
161
|
+
...state,
|
|
162
|
+
id: deviceId,
|
|
163
|
+
_cached: false,
|
|
164
|
+
_updatedAt: Date.now(),
|
|
165
|
+
});
|
|
166
|
+
this.dirty = true;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Update device from API status
|
|
170
|
+
*/
|
|
171
|
+
updateFromStatus(deviceId, status) {
|
|
172
|
+
this.updateDevice(deviceId, {
|
|
173
|
+
power: status.power,
|
|
174
|
+
brightness: status.brightness,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get cached state for a device
|
|
179
|
+
*/
|
|
180
|
+
getDevice(deviceId) {
|
|
181
|
+
this.load(); // Ensure loaded
|
|
182
|
+
return this.deviceStates.get(deviceId) || null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Check if device has fresh cached data
|
|
186
|
+
*/
|
|
187
|
+
hasFreshCache(deviceId, maxAge = this.maxAge) {
|
|
188
|
+
const state = this.getDevice(deviceId);
|
|
189
|
+
if (!state) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const updatedAt = state._updatedAt || state._cachedAt || 0;
|
|
193
|
+
return Date.now() - updatedAt < maxAge;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get device status from cache (for fallback)
|
|
197
|
+
*/
|
|
198
|
+
getCachedStatus(deviceId) {
|
|
199
|
+
const state = this.getDevice(deviceId);
|
|
200
|
+
if (!state || !state.power) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
id: state.id,
|
|
205
|
+
power: state.power,
|
|
206
|
+
brightness: state.brightness,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Remove a device from persistence
|
|
211
|
+
*/
|
|
212
|
+
removeDevice(deviceId) {
|
|
213
|
+
const deleted = this.deviceStates.delete(deviceId);
|
|
214
|
+
if (deleted) {
|
|
215
|
+
this.dirty = true;
|
|
216
|
+
}
|
|
217
|
+
return deleted;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Clear all persisted states
|
|
221
|
+
*/
|
|
222
|
+
clear() {
|
|
223
|
+
this.deviceStates.clear();
|
|
224
|
+
this.dirty = true;
|
|
225
|
+
try {
|
|
226
|
+
if (fs.existsSync(this.storagePath)) {
|
|
227
|
+
fs.unlinkSync(this.storagePath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Ignore delete errors
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get all cached device states
|
|
236
|
+
*/
|
|
237
|
+
getAllDevices() {
|
|
238
|
+
this.load();
|
|
239
|
+
return new Map(this.deviceStates);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get device count
|
|
243
|
+
*/
|
|
244
|
+
get size() {
|
|
245
|
+
return this.deviceStates.size;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Check if persistence has been modified
|
|
249
|
+
*/
|
|
250
|
+
get isDirty() {
|
|
251
|
+
return this.dirty;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get persistence statistics
|
|
255
|
+
*/
|
|
256
|
+
getStats() {
|
|
257
|
+
return {
|
|
258
|
+
deviceCount: this.size,
|
|
259
|
+
loaded: this.loaded,
|
|
260
|
+
dirty: this.dirty,
|
|
261
|
+
storagePath: this.storagePath,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.DevicePersistence = DevicePersistence;
|
|
266
|
+
/**
|
|
267
|
+
* Global persistence instance
|
|
268
|
+
*/
|
|
269
|
+
let globalPersistence = null;
|
|
270
|
+
/**
|
|
271
|
+
* Get or create the global persistence instance
|
|
272
|
+
*/
|
|
273
|
+
function getDevicePersistence(storagePath) {
|
|
274
|
+
if (!globalPersistence) {
|
|
275
|
+
globalPersistence = new DevicePersistence(storagePath);
|
|
276
|
+
}
|
|
277
|
+
return globalPersistence;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Reset the global persistence (for testing)
|
|
281
|
+
*/
|
|
282
|
+
function resetGlobalPersistence() {
|
|
283
|
+
globalPersistence = null;
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/api/persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 tbaur
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
* See LICENSE file for full license text
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Token bucket rate limiter for API requests
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Rate limiter configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface RateLimiterConfig {
|
|
13
|
+
/** Maximum requests per window */
|
|
14
|
+
maxRequests: number;
|
|
15
|
+
/** Window duration in milliseconds */
|
|
16
|
+
windowMs: number;
|
|
17
|
+
/** Whether to wait when rate limited (vs throwing immediately) */
|
|
18
|
+
waitOnLimit?: boolean;
|
|
19
|
+
/** Maximum wait time when waitOnLimit is true */
|
|
20
|
+
maxWaitMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Default rate limiter configuration
|
|
24
|
+
* Leviton allows max 99 devices per residence, 20 residences per account
|
|
25
|
+
* At startup: 99 devices × 2 queries (power + brightness) = 198 requests
|
|
26
|
+
* Rate limiter only applies to WRITE operations
|
|
27
|
+
*/
|
|
28
|
+
export declare const DEFAULT_RATE_LIMITER_CONFIG: RateLimiterConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Token bucket rate limiter
|
|
31
|
+
* Prevents overwhelming the Leviton API with too many requests
|
|
32
|
+
*/
|
|
33
|
+
export declare class RateLimiter {
|
|
34
|
+
private readonly maxRequests;
|
|
35
|
+
private readonly windowMs;
|
|
36
|
+
private readonly waitOnLimit;
|
|
37
|
+
private readonly maxWaitMs;
|
|
38
|
+
private requests;
|
|
39
|
+
constructor(config?: Partial<RateLimiterConfig>);
|
|
40
|
+
/**
|
|
41
|
+
* Clean up expired request timestamps
|
|
42
|
+
*/
|
|
43
|
+
private cleanup;
|
|
44
|
+
/**
|
|
45
|
+
* Get current request count in window
|
|
46
|
+
*/
|
|
47
|
+
get currentCount(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Get remaining requests in window
|
|
50
|
+
*/
|
|
51
|
+
get remaining(): number;
|
|
52
|
+
/**
|
|
53
|
+
* Get time until window resets (oldest request expires)
|
|
54
|
+
*/
|
|
55
|
+
get resetTime(): number;
|
|
56
|
+
/**
|
|
57
|
+
* Check if a request can be made without blocking
|
|
58
|
+
*/
|
|
59
|
+
canAcquire(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Try to acquire a request token
|
|
62
|
+
* @returns true if acquired, false if rate limited
|
|
63
|
+
*/
|
|
64
|
+
tryAcquire(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Acquire a request token, throwing if rate limited
|
|
67
|
+
* @throws RateLimitError if rate limited and waitOnLimit is false
|
|
68
|
+
*/
|
|
69
|
+
acquire(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Acquire a request token, waiting if necessary
|
|
72
|
+
* @throws RateLimitError if wait exceeds maxWaitMs
|
|
73
|
+
*/
|
|
74
|
+
acquireAsync(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Execute a function with rate limiting
|
|
77
|
+
*/
|
|
78
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Reset the rate limiter
|
|
81
|
+
*/
|
|
82
|
+
reset(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Get rate limiter status for monitoring
|
|
85
|
+
*/
|
|
86
|
+
getStatus(): {
|
|
87
|
+
currentCount: number;
|
|
88
|
+
maxRequests: number;
|
|
89
|
+
remaining: number;
|
|
90
|
+
resetTime: number;
|
|
91
|
+
windowMs: number;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get or create the global rate limiter
|
|
96
|
+
*/
|
|
97
|
+
export declare function getRateLimiter(config?: Partial<RateLimiterConfig>): RateLimiter;
|
|
98
|
+
/**
|
|
99
|
+
* Reset the global rate limiter (for testing)
|
|
100
|
+
*/
|
|
101
|
+
export declare function resetGlobalRateLimiter(): void;
|
|
102
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/api/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,EAAE,iBAKzC,CAAA;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAe;gBAEnB,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAQnD;;OAEG;IACH,OAAO,CAAC,OAAO;IAKf;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAGzB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAItB;IAED;;OAEG;IACH,UAAU,IAAI,OAAO;IAKrB;;;OAGG;IACH,UAAU,IAAI,OAAO;IAWrB;;;OAGG;IACH,OAAO,IAAI,IAAI;IAUf;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBnC;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IASlD;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,SAAS,IAAI;QACX,YAAY,EAAE,MAAM,CAAA;QACpB,WAAW,EAAE,MAAM,CAAA;QACnB,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,EAAE,MAAM,CAAA;KACjB;CASF;AAOD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,WAAW,CAK/E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 tbaur
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
* See LICENSE file for full license text
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Token bucket rate limiter for API requests
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.RateLimiter = exports.DEFAULT_RATE_LIMITER_CONFIG = void 0;
|
|
12
|
+
exports.getRateLimiter = getRateLimiter;
|
|
13
|
+
exports.resetGlobalRateLimiter = resetGlobalRateLimiter;
|
|
14
|
+
const errors_1 = require("../errors");
|
|
15
|
+
const retry_1 = require("../utils/retry");
|
|
16
|
+
/**
|
|
17
|
+
* Default rate limiter configuration
|
|
18
|
+
* Leviton allows max 99 devices per residence, 20 residences per account
|
|
19
|
+
* At startup: 99 devices × 2 queries (power + brightness) = 198 requests
|
|
20
|
+
* Rate limiter only applies to WRITE operations
|
|
21
|
+
*/
|
|
22
|
+
exports.DEFAULT_RATE_LIMITER_CONFIG = {
|
|
23
|
+
maxRequests: 300,
|
|
24
|
+
windowMs: 60000, // 1 minute
|
|
25
|
+
waitOnLimit: false,
|
|
26
|
+
maxWaitMs: 30000,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Token bucket rate limiter
|
|
30
|
+
* Prevents overwhelming the Leviton API with too many requests
|
|
31
|
+
*/
|
|
32
|
+
class RateLimiter {
|
|
33
|
+
maxRequests;
|
|
34
|
+
windowMs;
|
|
35
|
+
waitOnLimit;
|
|
36
|
+
maxWaitMs;
|
|
37
|
+
requests = [];
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
const merged = { ...exports.DEFAULT_RATE_LIMITER_CONFIG, ...config };
|
|
40
|
+
this.maxRequests = merged.maxRequests;
|
|
41
|
+
this.windowMs = merged.windowMs;
|
|
42
|
+
this.waitOnLimit = merged.waitOnLimit ?? false;
|
|
43
|
+
this.maxWaitMs = merged.maxWaitMs ?? 30000;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clean up expired request timestamps
|
|
47
|
+
*/
|
|
48
|
+
cleanup() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
this.requests = this.requests.filter(timestamp => now - timestamp < this.windowMs);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get current request count in window
|
|
54
|
+
*/
|
|
55
|
+
get currentCount() {
|
|
56
|
+
this.cleanup();
|
|
57
|
+
return this.requests.length;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get remaining requests in window
|
|
61
|
+
*/
|
|
62
|
+
get remaining() {
|
|
63
|
+
return Math.max(0, this.maxRequests - this.currentCount);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get time until window resets (oldest request expires)
|
|
67
|
+
*/
|
|
68
|
+
get resetTime() {
|
|
69
|
+
if (this.requests.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const oldest = Math.min(...this.requests);
|
|
73
|
+
return Math.max(0, this.windowMs - (Date.now() - oldest));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if a request can be made without blocking
|
|
77
|
+
*/
|
|
78
|
+
canAcquire() {
|
|
79
|
+
this.cleanup();
|
|
80
|
+
return this.requests.length < this.maxRequests;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Try to acquire a request token
|
|
84
|
+
* @returns true if acquired, false if rate limited
|
|
85
|
+
*/
|
|
86
|
+
tryAcquire() {
|
|
87
|
+
this.cleanup();
|
|
88
|
+
if (this.requests.length >= this.maxRequests) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
this.requests.push(Date.now());
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Acquire a request token, throwing if rate limited
|
|
96
|
+
* @throws RateLimitError if rate limited and waitOnLimit is false
|
|
97
|
+
*/
|
|
98
|
+
acquire() {
|
|
99
|
+
if (!this.tryAcquire()) {
|
|
100
|
+
const retryAfter = Math.ceil(this.resetTime / 1000);
|
|
101
|
+
throw new errors_1.RateLimitError(`Rate limit exceeded. ${this.maxRequests} requests per ${this.windowMs / 1000}s`, retryAfter);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Acquire a request token, waiting if necessary
|
|
106
|
+
* @throws RateLimitError if wait exceeds maxWaitMs
|
|
107
|
+
*/
|
|
108
|
+
async acquireAsync() {
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
while (!this.tryAcquire()) {
|
|
111
|
+
const elapsed = Date.now() - startTime;
|
|
112
|
+
if (elapsed >= this.maxWaitMs) {
|
|
113
|
+
throw new errors_1.RateLimitError(`Rate limit wait exceeded ${this.maxWaitMs}ms`, Math.ceil((this.maxWaitMs - elapsed) / 1000));
|
|
114
|
+
}
|
|
115
|
+
// Wait for the oldest request to expire
|
|
116
|
+
const waitTime = Math.min(this.resetTime, this.maxWaitMs - elapsed, 1000);
|
|
117
|
+
if (waitTime > 0) {
|
|
118
|
+
await (0, retry_1.sleep)(waitTime);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Execute a function with rate limiting
|
|
124
|
+
*/
|
|
125
|
+
async execute(fn) {
|
|
126
|
+
if (this.waitOnLimit) {
|
|
127
|
+
await this.acquireAsync();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.acquire();
|
|
131
|
+
}
|
|
132
|
+
return fn();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reset the rate limiter
|
|
136
|
+
*/
|
|
137
|
+
reset() {
|
|
138
|
+
this.requests = [];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get rate limiter status for monitoring
|
|
142
|
+
*/
|
|
143
|
+
getStatus() {
|
|
144
|
+
return {
|
|
145
|
+
currentCount: this.currentCount,
|
|
146
|
+
maxRequests: this.maxRequests,
|
|
147
|
+
remaining: this.remaining,
|
|
148
|
+
resetTime: this.resetTime,
|
|
149
|
+
windowMs: this.windowMs,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.RateLimiter = RateLimiter;
|
|
154
|
+
/**
|
|
155
|
+
* Global rate limiter instance
|
|
156
|
+
*/
|
|
157
|
+
let globalRateLimiter = null;
|
|
158
|
+
/**
|
|
159
|
+
* Get or create the global rate limiter
|
|
160
|
+
*/
|
|
161
|
+
function getRateLimiter(config) {
|
|
162
|
+
if (!globalRateLimiter) {
|
|
163
|
+
globalRateLimiter = new RateLimiter(config);
|
|
164
|
+
}
|
|
165
|
+
return globalRateLimiter;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Reset the global rate limiter (for testing)
|
|
169
|
+
*/
|
|
170
|
+
function resetGlobalRateLimiter() {
|
|
171
|
+
globalRateLimiter = null;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/api/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 tbaur
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
* See LICENSE file for full license text
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Request queue with priority and deduplication
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Request queue configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface RequestQueueConfig {
|
|
13
|
+
/** Maximum concurrent requests */
|
|
14
|
+
maxConcurrent: number;
|
|
15
|
+
/** Maximum queue size */
|
|
16
|
+
maxQueueSize: number;
|
|
17
|
+
/** Request timeout in ms */
|
|
18
|
+
requestTimeout: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Default request queue configuration
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_REQUEST_QUEUE_CONFIG: RequestQueueConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Request queue with priority ordering and deduplication
|
|
26
|
+
*/
|
|
27
|
+
export declare class RequestQueue {
|
|
28
|
+
private readonly maxConcurrent;
|
|
29
|
+
private readonly maxQueueSize;
|
|
30
|
+
private readonly requestTimeout;
|
|
31
|
+
private queue;
|
|
32
|
+
private inFlight;
|
|
33
|
+
private processing;
|
|
34
|
+
constructor(config?: Partial<RequestQueueConfig>);
|
|
35
|
+
/**
|
|
36
|
+
* Get queue length
|
|
37
|
+
*/
|
|
38
|
+
get length(): number;
|
|
39
|
+
/**
|
|
40
|
+
* Get number of in-flight requests
|
|
41
|
+
*/
|
|
42
|
+
get inFlightCount(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Check if queue is full
|
|
45
|
+
*/
|
|
46
|
+
get isFull(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Add a request to the queue
|
|
49
|
+
*/
|
|
50
|
+
add<T>(execute: () => Promise<T>, options?: {
|
|
51
|
+
priority?: 'high' | 'normal' | 'low';
|
|
52
|
+
dedupeKey?: string;
|
|
53
|
+
}): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Insert request into queue by priority
|
|
56
|
+
*/
|
|
57
|
+
private enqueue;
|
|
58
|
+
/**
|
|
59
|
+
* Process queued requests
|
|
60
|
+
*/
|
|
61
|
+
private processQueue;
|
|
62
|
+
/**
|
|
63
|
+
* Execute a single request with timeout
|
|
64
|
+
*/
|
|
65
|
+
private executeRequest;
|
|
66
|
+
/**
|
|
67
|
+
* Clear the queue
|
|
68
|
+
*/
|
|
69
|
+
clear(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Get queue statistics
|
|
72
|
+
*/
|
|
73
|
+
getStats(): {
|
|
74
|
+
queueLength: number;
|
|
75
|
+
inFlight: number;
|
|
76
|
+
maxConcurrent: number;
|
|
77
|
+
maxQueueSize: number;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Wait for all in-flight requests to complete
|
|
81
|
+
*/
|
|
82
|
+
drain(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* In-flight request deduplication map
|
|
86
|
+
*/
|
|
87
|
+
export declare class RequestDeduplicator {
|
|
88
|
+
private readonly inFlight;
|
|
89
|
+
private readonly maxSize;
|
|
90
|
+
constructor(maxSize?: number);
|
|
91
|
+
/**
|
|
92
|
+
* Execute a request with deduplication
|
|
93
|
+
*/
|
|
94
|
+
execute<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
95
|
+
/**
|
|
96
|
+
* Get number of in-flight requests
|
|
97
|
+
*/
|
|
98
|
+
get size(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Clear all tracked requests
|
|
101
|
+
*/
|
|
102
|
+
clear(): void;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=request-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-queue.d.ts","sourceRoot":"","sources":["../../src/api/request-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,kCAAkC;IAClC,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,4BAA4B;IAC5B,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,4BAA4B,EAAE,kBAI1C,CAAA;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IAEvC,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,QAAQ,CAA2C;IAC3D,OAAO,CAAC,UAAU,CAAQ;gBAEd,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM;IAOpD;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,GAAG,CAAC,CAAC,EACH,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;QACP,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;QACpC,SAAS,CAAC,EAAE,MAAM,CAAA;KACd,GACL,OAAO,CAAC,CAAC,CAAC;IA4Bb;;OAEG;IACH,OAAO,CAAC,OAAO;IAiBf;;OAEG;YACW,YAAY;IAwB1B;;OAEG;YACW,cAAc;IAsB5B;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,QAAQ,IAAI;QACV,WAAW,EAAE,MAAM,CAAA;QACnB,QAAQ,EAAE,MAAM,CAAA;QAChB,aAAa,EAAE,MAAM,CAAA;QACrB,YAAY,EAAE,MAAM,CAAA;KACrB;IASD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IACpE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;gBAEpB,OAAO,SAAM;IAIzB;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAwB/D;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd"}
|