@xtr-dev/rondevu-server 0.0.1 → 0.1.1

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/src/storage/kv.ts DELETED
@@ -1,241 +0,0 @@
1
- import { Storage, Session } from './types.ts';
2
-
3
- /**
4
- * Cloudflare KV storage adapter for session management
5
- */
6
- export class KVStorage implements Storage {
7
- private kv: KVNamespace;
8
-
9
- /**
10
- * Creates a new KV storage instance
11
- * @param kv Cloudflare KV namespace binding
12
- */
13
- constructor(kv: KVNamespace) {
14
- this.kv = kv;
15
- }
16
-
17
- /**
18
- * Generates a unique code using Web Crypto API
19
- */
20
- private generateCode(): string {
21
- return crypto.randomUUID();
22
- }
23
-
24
- /**
25
- * Gets the key for storing a session
26
- */
27
- private sessionKey(code: string): string {
28
- return `session:${code}`;
29
- }
30
-
31
- /**
32
- * Gets the key for the topic index
33
- */
34
- private topicIndexKey(origin: string, topic: string): string {
35
- return `index:${origin}:${topic}`;
36
- }
37
-
38
- async createSession(origin: string, topic: string, info: string, offer: string, expiresAt: number): Promise<string> {
39
- // Validate info length
40
- if (info.length > 1024) {
41
- throw new Error('Info string must be 1024 characters or less');
42
- }
43
-
44
- const code = this.generateCode();
45
- const createdAt = Date.now();
46
-
47
- const session: Session = {
48
- code,
49
- origin,
50
- topic,
51
- info,
52
- offer,
53
- answer: undefined,
54
- offerCandidates: [],
55
- answerCandidates: [],
56
- createdAt,
57
- expiresAt,
58
- };
59
-
60
- // Calculate TTL in seconds for KV
61
- const ttl = Math.max(60, Math.floor((expiresAt - createdAt) / 1000));
62
-
63
- // Store the session
64
- await this.kv.put(
65
- this.sessionKey(code),
66
- JSON.stringify(session),
67
- { expirationTtl: ttl }
68
- );
69
-
70
- // Update the topic index
71
- const indexKey = this.topicIndexKey(origin, topic);
72
- const existingIndex = await this.kv.get(indexKey, 'json') as string[] | null;
73
- const updatedIndex = existingIndex ? [...existingIndex, code] : [code];
74
-
75
- // Set index TTL to slightly longer than session TTL to avoid race conditions
76
- await this.kv.put(
77
- indexKey,
78
- JSON.stringify(updatedIndex),
79
- { expirationTtl: ttl + 300 }
80
- );
81
-
82
- return code;
83
- }
84
-
85
- async listSessionsByTopic(origin: string, topic: string): Promise<Session[]> {
86
- const indexKey = this.topicIndexKey(origin, topic);
87
- const codes = await this.kv.get(indexKey, 'json') as string[] | null;
88
-
89
- if (!codes || codes.length === 0) {
90
- return [];
91
- }
92
-
93
- // Fetch all sessions in parallel
94
- const sessionPromises = codes.map(async (code) => {
95
- const sessionData = await this.kv.get(this.sessionKey(code), 'json') as Session | null;
96
- return sessionData;
97
- });
98
-
99
- const sessions = await Promise.all(sessionPromises);
100
-
101
- // Filter out expired or answered sessions, and null values
102
- const now = Date.now();
103
- const validSessions = sessions.filter(
104
- (session): session is Session =>
105
- session !== null &&
106
- session.expiresAt > now &&
107
- session.answer === undefined
108
- );
109
-
110
- // Sort by creation time (newest first)
111
- return validSessions.sort((a, b) => b.createdAt - a.createdAt);
112
- }
113
-
114
- async listTopics(origin: string, page: number, limit: number): Promise<{
115
- topics: Array<{ topic: string; count: number }>;
116
- pagination: {
117
- page: number;
118
- limit: number;
119
- total: number;
120
- hasMore: boolean;
121
- };
122
- }> {
123
- // Ensure limit doesn't exceed 1000
124
- const safeLimit = Math.min(Math.max(1, limit), 1000);
125
- const safePage = Math.max(1, page);
126
-
127
- const prefix = `index:${origin}:`;
128
- const topicCounts = new Map<string, number>();
129
-
130
- // List all index keys for this origin
131
- const list = await this.kv.list({ prefix });
132
-
133
- // Process each topic index
134
- for (const key of list.keys) {
135
- // Extract topic from key: "index:{origin}:{topic}"
136
- const topic = key.name.substring(prefix.length);
137
-
138
- // Get the session codes for this topic
139
- const codes = await this.kv.get(key.name, 'json') as string[] | null;
140
-
141
- if (!codes || codes.length === 0) {
142
- continue;
143
- }
144
-
145
- // Fetch sessions to count only valid ones (unexpired and unanswered)
146
- const sessionPromises = codes.map(async (code) => {
147
- const sessionData = await this.kv.get(this.sessionKey(code), 'json') as Session | null;
148
- return sessionData;
149
- });
150
-
151
- const sessions = await Promise.all(sessionPromises);
152
-
153
- // Count valid sessions
154
- const now = Date.now();
155
- const validCount = sessions.filter(
156
- (session) =>
157
- session !== null &&
158
- session.expiresAt > now &&
159
- session.answer === undefined
160
- ).length;
161
-
162
- if (validCount > 0) {
163
- topicCounts.set(topic, validCount);
164
- }
165
- }
166
-
167
- // Convert to array and sort by topic name
168
- const allTopics = Array.from(topicCounts.entries())
169
- .map(([topic, count]) => ({ topic, count }))
170
- .sort((a, b) => a.topic.localeCompare(b.topic));
171
-
172
- // Apply pagination
173
- const total = allTopics.length;
174
- const offset = (safePage - 1) * safeLimit;
175
- const topics = allTopics.slice(offset, offset + safeLimit);
176
-
177
- return {
178
- topics,
179
- pagination: {
180
- page: safePage,
181
- limit: safeLimit,
182
- total,
183
- hasMore: offset + topics.length < total,
184
- },
185
- };
186
- }
187
-
188
- async getSession(code: string, origin: string): Promise<Session | null> {
189
- const sessionData = await this.kv.get(this.sessionKey(code), 'json') as Session | null;
190
-
191
- if (!sessionData) {
192
- return null;
193
- }
194
-
195
- // Validate origin and expiration
196
- if (sessionData.origin !== origin || sessionData.expiresAt <= Date.now()) {
197
- return null;
198
- }
199
-
200
- return sessionData;
201
- }
202
-
203
- async updateSession(code: string, origin: string, update: Partial<Session>): Promise<void> {
204
- const current = await this.getSession(code, origin);
205
-
206
- if (!current) {
207
- throw new Error('Session not found or origin mismatch');
208
- }
209
-
210
- // Merge updates
211
- const updated: Session = {
212
- ...current,
213
- ...(update.answer !== undefined && { answer: update.answer }),
214
- ...(update.offerCandidates !== undefined && { offerCandidates: update.offerCandidates }),
215
- ...(update.answerCandidates !== undefined && { answerCandidates: update.answerCandidates }),
216
- };
217
-
218
- // Calculate remaining TTL
219
- const ttl = Math.max(60, Math.floor((updated.expiresAt - Date.now()) / 1000));
220
-
221
- // Update the session
222
- await this.kv.put(
223
- this.sessionKey(code),
224
- JSON.stringify(updated),
225
- { expirationTtl: ttl }
226
- );
227
- }
228
-
229
- async deleteSession(code: string): Promise<void> {
230
- await this.kv.delete(this.sessionKey(code));
231
- }
232
-
233
- async cleanup(): Promise<void> {
234
- // KV automatically expires keys based on TTL
235
- // No manual cleanup needed
236
- }
237
-
238
- async close(): Promise<void> {
239
- // No connection to close for KV
240
- }
241
- }