Spaces:
Sleeping
Sleeping
| /** | |
| * services/sessionManager.js | |
| * ืื ืื ืืืืืจืื (In-Memory Session Store) | |
| * ======================================= | |
| * ืชืคืงืื: ืืฉืืืจ ืืช ืืขืืื ืืืช ืืกืืืืก ืฉื ืื ืืฉืชืืฉ. | |
| * ืืขืจื: ืืืืฆืืจ ืืืืชื ืืืืืคืื ืืช ืื ื-Redis, ืืื ืืคืืชืื ืื ืืขืืื. | |
| */ | |
| const sessions = {}; | |
| // ืืืืจืช ืืื ืชืคืืื ืืฉืืื (30 ืืงืืช) | |
| const SESSION_TIMEOUT = 30 * 60 * 1000; | |
| // ืืืืช ืืงืกืืืืืช ืฉื ืืืื ืืงืฉืืช ืืฉืืืจื (ืืื ืืขืช Memory Leak) | |
| const MAX_PROCESSED_REQUESTS = 50; | |
| // Spec v5.7: Max history items (5 turns = 10 items) | |
| const MAX_HISTORY_ITEMS = 10; | |
| function getSession(userId) { | |
| if (!sessions[userId]) { | |
| console.log(`โจ New Session created for: ${userId}`); | |
| sessions[userId] = { | |
| id: userId, | |
| cart: [], // ืืืืฆืจืื ืฉื ืืกืคื ืืขืืื | |
| currentProduct: null, // ืืืืฆืจ ืฉืขืืื ืืืืจืื ืืจืืข | |
| active_products: [], // [Spec v5.7.1] ืืืื ืฉืืื ืฉืืืจ ืกืืืืก ืฉื ืืืฆืจืื: draft, priced | |
| processedRequests: new Map(), | |
| lastActive: Date.now(), | |
| lastSpecChangeTime: Date.now(), // Tracking for Semantic Aging | |
| blockState: { reason: null, ttl: 0 }, // Phase 4.2: Built-in Turn-Based Memory | |
| // Spec v5.7: Stateful Advisor Additions | |
| history: [], | |
| lastBotMessage: null | |
| }; | |
| } | |
| const session = sessions[userId]; | |
| // Spec v5.7: History Management Helper | |
| if (!session.addToHistory) { | |
| session.addToHistory = (role, text) => { | |
| session.history.push({ role, text }); | |
| if (session.history.length > MAX_HISTORY_ITEMS) { | |
| session.history.shift(); | |
| } | |
| }; | |
| } | |
| // Phase 5.3: Semantic Aging (Drift Detection) | |
| const driftDelta = (Date.now() - session.lastSpecChangeTime) / 1000 / 60; // in minutes | |
| if (driftDelta >= 8 && driftDelta <= 12 && session.active_products && session.active_products.length > 0) { | |
| session.driftDetected = true; | |
| console.log(`โณ [SESSION] Semantic Aging detected for ${userId}: ${driftDelta.toFixed(1)}m`); | |
| } else { | |
| session.driftDetected = false; | |
| } | |
| if (session.blockState && session.blockState.reason === "PARAMETER_INSTABILITY") { | |
| if (session.blockState.ttl > 0) { | |
| session.blockState.ttl--; | |
| console.log(`[SESSION] TTL Decrement: ${session.blockState.ttl} left for ${userId}`); | |
| if (session.blockState.ttl === 0) { | |
| console.log(`[SESSION] TTL Expired. Releasing block for ${userId}`); | |
| session.blockState.reason = null; | |
| } | |
| } | |
| } | |
| // ืขืืืื ืืื ืคืขืืืืช ืืืจืื | |
| sessions[userId].lastActive = Date.now(); | |
| return sessions[userId]; | |
| } | |
| /** | |
| * Phase 1.2: Idempotency Guard (Check & Lock) | |
| * ืืืืง ืื ืืืงืฉื ืืืจ ืงืืืืช/ืืขืืืืช, ืื ืืขื ืืืชื ืืืืคื ืกืื ืืจืื ื | |
| */ | |
| function checkAndLockRequest(session, requestId, payloadText) { | |
| if (!requestId) return { status: 'NEW' }; | |
| const existing = session.processedRequests.get(requestId); | |
| if (existing) { | |
| if (existing.payload !== payloadText) { | |
| console.warn(`๐จ SECURITY ALERT: Payload Mismatch for requestId ${requestId}`); | |
| return { status: 'MISMATCH_ERROR' }; | |
| } | |
| if (existing.status === 'PROCESSING') { | |
| return { status: 'PROCESSING_ERROR' }; | |
| } | |
| if (existing.status === 'COMPLETED') { | |
| console.log(`โป๏ธ IDEMPOTENCY: Returning cached response for ${requestId}`); | |
| return { status: 'COMPLETED', cachedResponse: existing.response }; | |
| } | |
| } | |
| session.processedRequests.set(requestId, { | |
| payload: payloadText, | |
| status: 'PROCESSING', | |
| timestamp: Date.now(), | |
| response: null | |
| }); | |
| if (session.processedRequests.size > MAX_PROCESSED_REQUESTS) { | |
| const oldestKey = session.processedRequests.keys().next().value; | |
| session.processedRequests.delete(oldestKey); | |
| } | |
| return { status: 'NEW' }; | |
| } | |
| function cacheCompletedRequest(session, requestId, responseBody) { | |
| if (!requestId) return; | |
| const reqData = session.processedRequests.get(requestId); | |
| if (reqData && reqData.status === 'PROCESSING') { | |
| reqData.status = 'COMPLETED'; | |
| reqData.response = JSON.parse(JSON.stringify(responseBody)); | |
| } | |
| } | |
| function releaseFailedRequest(session, requestId) { | |
| if (!requestId) return; | |
| const reqData = session.processedRequests.get(requestId); | |
| if (reqData && reqData.status === 'PROCESSING') { | |
| session.processedRequests.delete(requestId); | |
| console.log(`๐ RELEASED Failed Request Lock: ${requestId}`); | |
| } | |
| } | |
| function clearSession(userId) { | |
| if (sessions[userId]) { | |
| sessions[userId].currentProduct = null; | |
| sessions[userId].active_products = []; | |
| sessions[userId].history = []; | |
| sessions[userId].lastBotMessage = null; | |
| console.log(`๐งน Session context cleared for: ${userId}`); | |
| } | |
| } | |
| function clearCart(userId) { | |
| if (sessions[userId]) { | |
| sessions[userId].cart = []; | |
| sessions[userId].currentProduct = null; | |
| sessions[userId].active_products = []; | |
| sessions[userId].history = []; | |
| sessions[userId].lastBotMessage = null; | |
| console.log(`๐๏ธ Cart emptied for: ${userId}`); | |
| } | |
| } | |
| /** | |
| * Phase 4: Secure Push to Cart | |
| */ | |
| function pushToSessionCart(session, item) { | |
| if (!item.traceId) { | |
| throw new Error("Cannot push item without traceId to cart."); | |
| } | |
| const exists = session.cart.some(cartItem => cartItem.traceId === item.traceId); | |
| if (exists) { | |
| console.warn(`๐ก๏ธ [SESSION] Duplicate traceId blocked: ${item.traceId}`); | |
| return false; | |
| } | |
| session.cart.push(item); | |
| console.log(`โ [SESSION] Item added to cart: ${item.product} (${item.traceId.substring(0, 8)}...)`); | |
| return true; | |
| } | |
| // ืื ืื ืื ื ืืงืื ืืืืืืื ืืืืืจืื (Garbage Collection) | |
| setInterval(() => { | |
| const now = Date.now(); | |
| Object.keys(sessions).forEach(key => { | |
| if (now - sessions[key].lastActive > SESSION_TIMEOUT) { | |
| delete sessions[key]; | |
| } | |
| }); | |
| }, 60 * 1000); | |
| module.exports = { | |
| getSession, | |
| clearSession, | |
| clearCart, | |
| pushToSessionCart, | |
| checkAndLockRequest, | |
| cacheCompletedRequest, | |
| releaseFailedRequest | |
| }; |