forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1import type { SessionAdapter, SessionData } from "../types.ts";
2import { DatabaseSync } from "node:sqlite";
3
4interface SessionTable {
5 session_id: string;
6 user_id: string;
7 handle: string | null;
8 is_authenticated: number;
9 data: string | null;
10 created_at: number;
11 expires_at: number;
12 last_accessed_at: number;
13}
14
15export class SQLiteAdapter implements SessionAdapter {
16 private db: DatabaseSync;
17
18 constructor(databasePath: string) {
19 // Handle sqlite:// URLs or direct paths
20 const dbPath = databasePath.startsWith("sqlite://")
21 ? databasePath.slice(9)
22 : databasePath;
23
24 this.db = new DatabaseSync(dbPath);
25 this.initializeDatabase();
26 }
27
28 private initializeDatabase() {
29 this.db.exec(`
30 CREATE TABLE IF NOT EXISTS sessions (
31 session_id TEXT PRIMARY KEY,
32 user_id TEXT NOT NULL,
33 handle TEXT,
34 is_authenticated INTEGER NOT NULL DEFAULT 1,
35 data TEXT, -- JSON string
36 created_at INTEGER NOT NULL,
37 expires_at INTEGER NOT NULL,
38 last_accessed_at INTEGER NOT NULL
39 )
40 `);
41
42 // Index for cleanup operations
43 this.db.exec(`
44 CREATE INDEX IF NOT EXISTS idx_sessions_expires_at
45 ON sessions(expires_at)
46 `);
47
48 // Index for user lookups
49 this.db.exec(`
50 CREATE INDEX IF NOT EXISTS idx_sessions_user_id
51 ON sessions(user_id)
52 `);
53 }
54
55 get(sessionId: string): Promise<SessionData | null> {
56 const stmt = this.db.prepare(`
57 SELECT * FROM sessions
58 WHERE session_id = ?
59 `);
60
61 const row = stmt.get(sessionId) as SessionTable | undefined;
62 if (!row) return Promise.resolve(null);
63
64 return Promise.resolve(this.rowToSessionData(row));
65 }
66
67 set(sessionId: string, data: SessionData): Promise<void> {
68 const stmt = this.db.prepare(`
69 INSERT OR REPLACE INTO sessions
70 (session_id, user_id, handle, is_authenticated, data, created_at, expires_at, last_accessed_at)
71 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
72 `);
73
74 stmt.run(
75 sessionId,
76 data.userId,
77 data.handle || null,
78 data.isAuthenticated ? 1 : 0,
79 data.data ? JSON.stringify(data.data) : null,
80 data.createdAt,
81 data.expiresAt,
82 data.lastAccessedAt
83 );
84 return Promise.resolve();
85 }
86
87 update(sessionId: string, updates: Partial<SessionData>): Promise<boolean> {
88 const setParts: string[] = [];
89 const values: (string | number | null)[] = [];
90
91 if (updates.userId !== undefined) {
92 setParts.push("user_id = ?");
93 values.push(updates.userId);
94 }
95
96 if (updates.handle !== undefined) {
97 setParts.push("handle = ?");
98 values.push(updates.handle);
99 }
100
101 if (updates.isAuthenticated !== undefined) {
102 setParts.push("is_authenticated = ?");
103 values.push(updates.isAuthenticated ? 1 : 0);
104 }
105
106 if (updates.data !== undefined) {
107 setParts.push("data = ?");
108 values.push(updates.data ? JSON.stringify(updates.data) : null);
109 }
110
111 if (updates.expiresAt !== undefined) {
112 setParts.push("expires_at = ?");
113 values.push(updates.expiresAt);
114 }
115
116 if (updates.lastAccessedAt !== undefined) {
117 setParts.push("last_accessed_at = ?");
118 values.push(updates.lastAccessedAt);
119 }
120
121 if (setParts.length === 0) return Promise.resolve(false);
122
123 values.push(sessionId);
124
125 const stmt = this.db.prepare(`
126 UPDATE sessions
127 SET ${setParts.join(", ")}
128 WHERE session_id = ?
129 `);
130
131 const result = stmt.run(...values);
132 return Promise.resolve(Number(result.changes) > 0);
133 }
134
135 delete(sessionId: string): Promise<void> {
136 const stmt = this.db.prepare("DELETE FROM sessions WHERE session_id = ?");
137 stmt.run(sessionId);
138 return Promise.resolve();
139 }
140
141 cleanup(expiresBeforeMs: number): Promise<number> {
142 const stmt = this.db.prepare("DELETE FROM sessions WHERE expires_at < ?");
143 const result = stmt.run(expiresBeforeMs);
144 return Promise.resolve(Number(result.changes));
145 }
146
147 exists(sessionId: string): Promise<boolean> {
148 const stmt = this.db.prepare(
149 "SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1"
150 );
151 return Promise.resolve(stmt.get(sessionId) !== undefined);
152 }
153
154 private rowToSessionData(row: SessionTable): SessionData {
155 return {
156 sessionId: row.session_id,
157 userId: row.user_id,
158 handle: row.handle || undefined,
159 isAuthenticated: Boolean(row.is_authenticated),
160 data: row.data ? JSON.parse(row.data) : undefined,
161 createdAt: row.created_at,
162 expiresAt: row.expires_at,
163 lastAccessedAt: row.last_accessed_at,
164 };
165 }
166
167 // SQLite-specific methods
168 close(): void {
169 this.db.close();
170 }
171
172 vacuum(): void {
173 this.db.exec("VACUUM");
174 }
175
176 getSessionsByUser(userId: string): SessionData[] {
177 const stmt = this.db.prepare("SELECT * FROM sessions WHERE user_id = ?");
178 const rows = stmt.all(userId) as unknown as SessionTable[];
179 return rows.map(row => this.rowToSessionData(row));
180 }
181}