Download server/sessionStore.js from sharktide/chat: direct link, hf CLI and curl.
- Browser
- Download file 8.63 kB
-
https://huggingface.co/spaces/sharktide/chat/resolve/126e920d80ae2b40ca9c88fd394ed76922c57742/server/sessionStore.js
- Command line
-
hf download hf://spaces/sharktide/chat@126e920d80ae2b40ca9c88fd394ed76922c57742/server/sessionStore.js
-
curl -L -o sessionStore.js https://huggingface.co/spaces/sharktide/chat/resolve/126e920d80ae2b40ca9c88fd394ed76922c57742/server/sessionStore.js
8.63 kB
| // sessionStore.js β access_token + Supabase RLS, no service role key needed. | |
| // Device sessions live in memory only (restart clears them). | |
| import { createClient } from '@supabase/supabase-js'; | |
| import crypto from 'crypto'; | |
| let _SUPABASE_URL, _SUPABASE_ANON_KEY; | |
| export function initStoreConfig(url, key) { _SUPABASE_URL = url; _SUPABASE_ANON_KEY = key; } | |
| const TEMP_TTL_MS = 24 * 60 * 60 * 1000; | |
| const TEMP_INACTIVITY = 12 * 60 * 60 * 1000; | |
| const TEMP_MSG_LIMIT = 15; | |
| const userCache = new Map(); // userId -> { sessions: Map, online: Set } | |
| const tempStore = new Map(); // tempId -> TempData | |
| const devSessions = new Map(); // token -> DeviceSession | |
| setInterval(() => { | |
| const now = Date.now(); | |
| for (const [id, d] of tempStore) | |
| if (now - d.created > TEMP_TTL_MS || now - d.lastActive > TEMP_INACTIVITY) | |
| tempStore.delete(id); | |
| }, 30 * 60 * 1000); | |
| function userClient(accessToken) { | |
| return createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, { | |
| global: { headers: { Authorization: `Bearer ${accessToken}` } }, | |
| auth: { persistSession: false }, | |
| }); | |
| } | |
| export const sessionStore = { | |
| // ββ TEMP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| initTemp(t) { | |
| if (!tempStore.has(t)) | |
| tempStore.set(t, { sessions: new Map(), msgCount: 0, created: Date.now(), lastActive: Date.now() }); | |
| return tempStore.get(t); | |
| }, | |
| tempCanSend(t) { const d = tempStore.get(t); return d ? d.msgCount < TEMP_MSG_LIMIT : false; }, | |
| tempBump(t) { const d = tempStore.get(t); if (d) { d.msgCount++; d.lastActive = Date.now(); } }, | |
| getTempSessions(t) { return [...(tempStore.get(t)?.sessions.values() || [])]; }, | |
| getTempSession(t, id) { return tempStore.get(t)?.sessions.get(id) || null; }, | |
| createTempSession(t) { | |
| const d = this.initTemp(t); | |
| const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] }; | |
| d.sessions.set(s.id, s); d.lastActive = Date.now(); return s; | |
| }, | |
| updateTempSession(t, id, patch) { | |
| const d = tempStore.get(t); if (!d) return null; | |
| const s = d.sessions.get(id); if (!s) return null; | |
| Object.assign(s, patch); d.lastActive = Date.now(); return s; | |
| }, | |
| deleteTempSession(t, id) { tempStore.get(t)?.sessions.delete(id); }, | |
| deleteTempAll(t) { tempStore.get(t)?.sessions.clear(); }, | |
| async transferTempToUser(tempId, userId, accessToken) { | |
| const d = tempStore.get(tempId); if (!d || !d.sessions.size) return; | |
| const uc = userClient(accessToken); | |
| const user = this._ensureUser(userId); | |
| for (const s of d.sessions.values()) { | |
| user.sessions.set(s.id, s); | |
| await this._persist(uc, userId, s).catch(() => {}); | |
| } | |
| tempStore.delete(tempId); | |
| }, | |
| // ββ USERS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _ensureUser(uid) { | |
| if (!userCache.has(uid)) userCache.set(uid, { sessions: new Map(), online: new Set() }); | |
| return userCache.get(uid); | |
| }, | |
| async loadUserSessions(userId, accessToken) { | |
| const uc = userClient(accessToken); | |
| const { data, error } = await uc.from('web_sessions').select('*') | |
| .eq('user_id', userId).order('updated_at', { ascending: false }); | |
| if (error) { console.error('loadUserSessions', error.message); return []; } | |
| const user = this._ensureUser(userId); | |
| for (const row of data || []) | |
| user.sessions.set(row.id, { id: row.id, name: row.name, | |
| created: new Date(row.created_at).getTime(), history: row.history || [], model: row.model }); | |
| return [...user.sessions.values()]; | |
| }, | |
| getUserSessions(uid) { return [...(userCache.get(uid)?.sessions.values() || [])]; }, | |
| getUserSession(uid, id) { return userCache.get(uid)?.sessions.get(id) || null; }, | |
| async createUserSession(userId, accessToken) { | |
| const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] }; | |
| this._ensureUser(userId).sessions.set(s.id, s); | |
| await this._persist(userClient(accessToken), userId, s).catch(() => {}); | |
| return s; | |
| }, | |
| async updateUserSession(userId, accessToken, sessionId, patch) { | |
| const user = userCache.get(userId); if (!user) { console.log ("No user for " + userId); return null; } | |
| const s = user.sessions.get(sessionId); if (!s) { console.log ("No session found for " + sessionId); return null; } | |
| Object.assign(s, patch); | |
| await this._persist(userClient(accessToken), userId, s).catch(() => {}); | |
| return s; | |
| }, | |
| async deleteUserSession(userId, accessToken, id) { | |
| // β Remove the .catch() from the query chain | |
| // β‘ Use try/catch to surface any unexpected errors | |
| try { | |
| // Remove the session from the inβmemory cache | |
| userCache.get(userId)?.sessions.delete(id); | |
| // Perform the actual DB delete | |
| const { error } = await userClient(accessToken) | |
| .from('web_sessions') | |
| .delete() | |
| .eq('id', id) | |
| .eq('user_id', userId); | |
| if (error) { | |
| console.error('Supabase delete error:', error.message); | |
| } | |
| } catch (ex) { | |
| console.error('Unexpected deleteUserSession error:', ex); | |
| } | |
| }, | |
| async deleteAllUserSessions(userId, accessToken) { | |
| // Clear the inβmemory store first | |
| const u = userCache.get(userId); | |
| if (u) { | |
| u.sessions.clear(); | |
| } else { | |
| console.log('No user for ' + userId); | |
| return null; | |
| } | |
| // Delete everything from Supabase | |
| try { | |
| const { error } = await userClient(accessToken) | |
| .from('web_sessions') | |
| .delete() | |
| .eq('user_id', userId); | |
| if (error) { | |
| console.error('Supabase bulk delete error:', error.message); | |
| } | |
| } catch (ex) { | |
| console.error('Unexpected deleteAllUserSessions error:', ex); | |
| } | |
| }, | |
| async _persist(uc, userId, s) { | |
| await uc.from('web_sessions').upsert({ | |
| id: s.id, user_id: userId, name: s.name, history: s.history || [], | |
| model: s.model || null, updated_at: new Date().toISOString(), | |
| created_at: new Date(s.created).toISOString(), | |
| }); | |
| }, | |
| markOnline(uid, ws) { this._ensureUser(uid).online.add(ws); }, | |
| markOffline(uid, ws) { userCache.get(uid)?.online.delete(ws); }, | |
| // ββ SHARE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async createShareToken(userId, accessToken, sessionId) { | |
| const s = this.getUserSession(userId, sessionId); if (!s) return null; | |
| const token = crypto.randomBytes(24).toString('base64url'); | |
| const uc = userClient(accessToken); | |
| const { error } = await uc.from('shared_sessions').insert({ | |
| token, owner_id: userId, session_snapshot: s, created_at: new Date().toISOString(), | |
| }); | |
| return error ? null : token; | |
| }, | |
| async resolveShareToken(token) { | |
| // shared_sessions SELECT is public via RLS | |
| const uc = createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, { auth: { persistSession: false } }); | |
| const { data } = await uc.from('shared_sessions').select('*').eq('token', token).single(); | |
| return data || null; | |
| }, | |
| async importSharedSession(userId, accessToken, token) { | |
| const shared = await this.resolveShareToken(token); if (!shared) return null; | |
| const snap = shared.session_snapshot; | |
| const newSession = { ...snap, id: crypto.randomUUID(), | |
| name: `${snap.name} (shared)`, created: Date.now() }; | |
| const uc = userClient(accessToken); | |
| this._ensureUser(userId).sessions.set(newSession.id, newSession); | |
| await this._persist(uc, userId, newSession).catch(() => {}); | |
| return newSession; | |
| }, | |
| }; | |
| export const deviceSessionStore = { | |
| create(userId, ip, userAgent) { | |
| const token = crypto.randomBytes(32).toString('hex'); | |
| devSessions.set(token, { token, userId, ip, userAgent, | |
| createdAt: new Date().toISOString(), lastSeen: new Date().toISOString(), active: true }); | |
| return token; | |
| }, | |
| getForUser(uid) { return [...devSessions.values()].filter(s => s.userId === uid); }, | |
| revoke(token) { const s = devSessions.get(token); if (s) s.active = false; }, | |
| revokeAllExcept(uid, except) { | |
| for (const [t, s] of devSessions) if (s.userId === uid && t !== except) s.active = false; | |
| }, | |
| validate(token) { | |
| const s = devSessions.get(token); if (!s || !s.active) return null; | |
| s.lastSeen = new Date().toISOString(); return s; | |
| }, | |
| }; | |