node-red-contrib-tplink-tapo-connect-api
Version:
This unofficial node-RED node allows connection to TP-Link Tapo devices. This project has been enhanced with AI support to enable new features. Starting with v0.50, we have added support for the KLAP protocol. To prioritize the operation of this node, we
192 lines • 6.17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionManager = exports.SessionState = void 0;
var SessionState;
(function (SessionState) {
SessionState["DISCONNECTED"] = "disconnected";
SessionState["CONNECTING"] = "connecting";
SessionState["CONNECTED"] = "connected";
SessionState["EXPIRED"] = "expired";
SessionState["ERROR"] = "error";
})(SessionState || (exports.SessionState = SessionState = {}));
/**
* Manages device sessions and session lifecycle
* Single responsibility: Session state and lifecycle management
*/
class SessionManager {
constructor(options = {}) {
var _a, _b, _c;
this.sessionData = {};
this.sessionState = SessionState.DISCONNECTED;
this.refreshPromise = null;
this.options = {
sessionTimeout: (_a = options.sessionTimeout) !== null && _a !== void 0 ? _a : 1800000, // 30 minutes
refreshThreshold: (_b = options.refreshThreshold) !== null && _b !== void 0 ? _b : 300000, // 5 minutes before expiry
maxRefreshAttempts: (_c = options.maxRefreshAttempts) !== null && _c !== void 0 ? _c : 3
};
}
/**
* Initialize a new session
*/
async initializeSession(sessionData) {
var _a;
this.sessionState = SessionState.CONNECTING;
try {
this.sessionData = {
...sessionData,
expiresAt: (_a = sessionData.expiresAt) !== null && _a !== void 0 ? _a : (Date.now() + this.options.sessionTimeout)
};
this.sessionState = SessionState.CONNECTED;
}
catch (error) {
this.sessionState = SessionState.ERROR;
throw error;
}
}
/**
* Get current session data
*/
getSessionData() {
return { ...this.sessionData };
}
/**
* Get current session state
*/
getSessionState() {
return this.sessionState;
}
/**
* Check if session is valid and not expired
*/
isSessionValid() {
if (this.sessionState !== SessionState.CONNECTED) {
return false;
}
if (this.sessionData.expiresAt && Date.now() >= this.sessionData.expiresAt) {
this.sessionState = SessionState.EXPIRED;
return false;
}
return true;
}
/**
* Check if session needs refresh (close to expiry)
*/
needsRefresh() {
if (!this.isSessionValid()) {
return true;
}
if (this.sessionData.expiresAt) {
const timeUntilExpiry = this.sessionData.expiresAt - Date.now();
return timeUntilExpiry <= this.options.refreshThreshold;
}
return false;
}
/**
* Refresh session if needed
*/
async refreshSessionIfNeeded(refreshFunction) {
if (!this.needsRefresh()) {
return;
}
// Prevent concurrent refresh attempts
if (this.refreshPromise) {
await this.refreshPromise;
return;
}
this.refreshPromise = this.performRefresh(refreshFunction);
try {
await this.refreshPromise;
}
finally {
this.refreshPromise = null;
}
}
/**
* Update session data
*/
updateSession(sessionData) {
this.sessionData = {
...this.sessionData,
...sessionData
};
// Update expiry if not provided
if (!sessionData.expiresAt && sessionData.token) {
this.sessionData.expiresAt = Date.now() + this.options.sessionTimeout;
}
}
/**
* Invalidate current session
*/
invalidateSession() {
this.sessionData = {};
this.sessionState = SessionState.DISCONNECTED;
}
/**
* Mark session as expired
*/
markExpired() {
this.sessionState = SessionState.EXPIRED;
}
/**
* Check if specific session error indicates session expiry
*/
isSessionError(error) {
const sessionErrorPatterns = [
'session expired',
'invalid terminal uuid',
'klap 1002',
'klap -1012',
'terminal uuid mismatch'
];
const errorMessage = error.message.toLowerCase();
return sessionErrorPatterns.some(pattern => errorMessage.includes(pattern));
}
/**
* Handle session error and update state accordingly
*/
handleSessionError(error) {
if (this.isSessionError(error)) {
this.sessionState = SessionState.EXPIRED;
}
else {
this.sessionState = SessionState.ERROR;
}
}
/**
* Get session headers for requests
*/
getSessionHeaders() {
const headers = {};
if (this.sessionData.cookies && this.sessionData.cookies.length > 0) {
headers['Cookie'] = this.sessionData.cookies.join('; ');
}
if (this.sessionData.token) {
headers['Authorization'] = `Bearer ${this.sessionData.token}`;
}
return headers;
}
/**
* Perform session refresh
*/
async performRefresh(refreshFunction) {
let lastError = null;
for (let attempt = 0; attempt < this.options.maxRefreshAttempts; attempt++) {
try {
const newSessionData = await refreshFunction();
await this.initializeSession(newSessionData);
return;
}
catch (error) {
lastError = error;
if (attempt < this.options.maxRefreshAttempts - 1) {
// Wait before retry with exponential backoff
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
}
}
}
this.sessionState = SessionState.ERROR;
throw new Error(`Failed to refresh session after ${this.options.maxRefreshAttempts} attempts: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
}
}
exports.SessionManager = SessionManager;
//# sourceMappingURL=session-manager.js.map