mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-10 10:53:24 +00:00
Simplify API: remove topics, rename session→offer
- Remove topic-based grouping and discovery - Rename sessions to offers for clarity - Simplify to just POST /offer, POST /answer, POST /poll - Add version to health endpoint - Update database schema (sessions→offers table) - Reduce offer timeout to 1 minute - Server version: 0.0.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
22
migrations/0002_remove_topics.sql
Normal file
22
migrations/0002_remove_topics.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Remove topics and rename sessions to offers
|
||||||
|
-- This is a breaking change requiring a fresh database
|
||||||
|
|
||||||
|
-- Drop old sessions table
|
||||||
|
DROP TABLE IF EXISTS sessions;
|
||||||
|
|
||||||
|
-- Create offers table (without topic)
|
||||||
|
CREATE TABLE offers (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
origin TEXT NOT NULL,
|
||||||
|
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
||||||
|
offer TEXT NOT NULL,
|
||||||
|
answer TEXT,
|
||||||
|
offer_candidates TEXT NOT NULL DEFAULT '[]',
|
||||||
|
answer_candidates TEXT NOT NULL DEFAULT '[]',
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for efficient queries
|
||||||
|
CREATE INDEX idx_offers_expires_at ON offers(expires_at);
|
||||||
|
CREATE INDEX idx_offers_origin ON offers(origin);
|
||||||
124
src/app.ts
124
src/app.ts
@@ -4,13 +4,13 @@ import { Storage } from './storage/types.ts';
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
sessionTimeout: number;
|
offerTimeout: number;
|
||||||
corsOrigins: string[];
|
corsOrigins: string[];
|
||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the origin for session isolation.
|
* Determines the origin for offer isolation.
|
||||||
* If X-Rondevu-Global header is set to 'true', returns the global origin (https://ronde.vu).
|
* If X-Rondevu-Global header is set to 'true', returns the global origin (https://ronde.vu).
|
||||||
* Otherwise, returns the request's Origin header.
|
* Otherwise, returns the request's Origin header.
|
||||||
*/
|
*/
|
||||||
@@ -60,80 +60,28 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /topics
|
* GET /health
|
||||||
* Lists all topics with their unanswered session counts (paginated)
|
* Health check endpoint with version
|
||||||
* Query params: page (default: 1), limit (default: 100, max: 1000)
|
|
||||||
*/
|
*/
|
||||||
app.get('/topics', async (c) => {
|
app.get('/health', (c) => {
|
||||||
try {
|
|
||||||
const origin = getOrigin(c);
|
|
||||||
const page = parseInt(c.req.query('page') || '1', 10);
|
|
||||||
const limit = parseInt(c.req.query('limit') || '100', 10);
|
|
||||||
|
|
||||||
const result = await storage.listTopics(origin, page, limit);
|
|
||||||
|
|
||||||
return c.json(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error listing topics:', err);
|
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /:topic/sessions
|
|
||||||
* Lists all unanswered sessions for a topic
|
|
||||||
*/
|
|
||||||
app.get('/:topic/sessions', async (c) => {
|
|
||||||
try {
|
|
||||||
const origin = getOrigin(c);
|
|
||||||
const topic = c.req.param('topic');
|
|
||||||
|
|
||||||
if (!topic) {
|
|
||||||
return c.json({ error: 'Missing required parameter: topic' }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.length > 1024) {
|
|
||||||
return c.json({ error: 'Topic string must be 1024 characters or less' }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessions = await storage.listSessionsByTopic(origin, topic);
|
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
sessions: sessions.map(s => ({
|
status: 'ok',
|
||||||
code: s.code,
|
timestamp: Date.now(),
|
||||||
peerId: s.peerId,
|
version: config.version || 'unknown'
|
||||||
offer: s.offer,
|
|
||||||
offerCandidates: s.offerCandidates,
|
|
||||||
createdAt: s.createdAt,
|
|
||||||
expiresAt: s.expiresAt,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
console.error('Error listing sessions:', err);
|
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /:topic/offer
|
* POST /offer
|
||||||
* Creates a new offer and returns a unique session code
|
* Creates a new offer and returns a unique code
|
||||||
* Body: { peerId: string, offer: string }
|
* Body: { peerId: string, offer: string, code?: string }
|
||||||
*/
|
*/
|
||||||
app.post('/:topic/offer', async (c) => {
|
app.post('/offer', async (c) => {
|
||||||
try {
|
try {
|
||||||
const origin = getOrigin(c);
|
const origin = getOrigin(c);
|
||||||
const topic = c.req.param('topic');
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { peerId, offer, code: customCode } = body;
|
const { peerId, offer, code: customCode } = body;
|
||||||
|
|
||||||
if (!topic || typeof topic !== 'string') {
|
|
||||||
return c.json({ error: 'Missing or invalid required parameter: topic' }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.length > 1024) {
|
|
||||||
return c.json({ error: 'Topic string must be 1024 characters or less' }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!peerId || typeof peerId !== 'string') {
|
if (!peerId || typeof peerId !== 'string') {
|
||||||
return c.json({ error: 'Missing or invalid required parameter: peerId' }, 400);
|
return c.json({ error: 'Missing or invalid required parameter: peerId' }, 400);
|
||||||
}
|
}
|
||||||
@@ -146,14 +94,14 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
return c.json({ error: 'Missing or invalid required parameter: offer' }, 400);
|
return c.json({ error: 'Missing or invalid required parameter: offer' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = Date.now() + config.sessionTimeout;
|
const expiresAt = Date.now() + config.offerTimeout;
|
||||||
const code = await storage.createSession(origin, topic, peerId, offer, expiresAt, customCode);
|
const code = await storage.createOffer(origin, peerId, offer, expiresAt, customCode);
|
||||||
|
|
||||||
return c.json({ code }, 200);
|
return c.json({ code }, 200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating offer:', err);
|
console.error('Error creating offer:', err);
|
||||||
|
|
||||||
// Check if it's a session code clash error
|
// Check if it's a code clash error
|
||||||
if (err instanceof Error && err.message.includes('already exists')) {
|
if (err instanceof Error && err.message.includes('already exists')) {
|
||||||
return c.json({ error: err.message }, 409);
|
return c.json({ error: err.message }, 409);
|
||||||
}
|
}
|
||||||
@@ -189,23 +137,23 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
return c.json({ error: 'Cannot provide both answer and candidate' }, 400);
|
return c.json({ error: 'Cannot provide both answer and candidate' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await storage.getSession(code, origin);
|
const offer = await storage.getOffer(code, origin);
|
||||||
|
|
||||||
if (!session) {
|
if (!offer) {
|
||||||
return c.json({ error: 'Session not found, expired, or origin mismatch' }, 404);
|
return c.json({ error: 'Offer not found, expired, or origin mismatch' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
await storage.updateSession(code, origin, { answer });
|
await storage.updateOffer(code, origin, { answer });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
if (side === 'offerer') {
|
if (side === 'offerer') {
|
||||||
const updatedCandidates = [...session.offerCandidates, candidate];
|
const updatedCandidates = [...offer.offerCandidates, candidate];
|
||||||
await storage.updateSession(code, origin, { offerCandidates: updatedCandidates });
|
await storage.updateOffer(code, origin, { offerCandidates: updatedCandidates });
|
||||||
} else {
|
} else {
|
||||||
const updatedCandidates = [...session.answerCandidates, candidate];
|
const updatedCandidates = [...offer.answerCandidates, candidate];
|
||||||
await storage.updateSession(code, origin, { answerCandidates: updatedCandidates });
|
await storage.updateOffer(code, origin, { answerCandidates: updatedCandidates });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +166,7 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /poll
|
* POST /poll
|
||||||
* Polls for session data (offer, answer, ICE candidates)
|
* Polls for offer data (offer, answer, ICE candidates)
|
||||||
* Body: { code: string, side: 'offerer' | 'answerer' }
|
* Body: { code: string, side: 'offerer' | 'answerer' }
|
||||||
*/
|
*/
|
||||||
app.post('/poll', async (c) => {
|
app.post('/poll', async (c) => {
|
||||||
@@ -235,36 +183,28 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
return c.json({ error: 'Invalid or missing parameter: side (must be "offerer" or "answerer")' }, 400);
|
return c.json({ error: 'Invalid or missing parameter: side (must be "offerer" or "answerer")' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await storage.getSession(code, origin);
|
const offer = await storage.getOffer(code, origin);
|
||||||
|
|
||||||
if (!session) {
|
if (!offer) {
|
||||||
return c.json({ error: 'Session not found, expired, or origin mismatch' }, 404);
|
return c.json({ error: 'Offer not found, expired, or origin mismatch' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (side === 'offerer') {
|
if (side === 'offerer') {
|
||||||
return c.json({
|
return c.json({
|
||||||
answer: session.answer || null,
|
answer: offer.answer || null,
|
||||||
answerCandidates: session.answerCandidates,
|
answerCandidates: offer.answerCandidates,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return c.json({
|
return c.json({
|
||||||
offer: session.offer,
|
offer: offer.offer,
|
||||||
offerCandidates: session.offerCandidates,
|
offerCandidates: offer.offerCandidates,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error polling session:', err);
|
console.error('Error polling offer:', err);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /health
|
|
||||||
* Health check endpoint
|
|
||||||
*/
|
|
||||||
app.get('/health', (c) => {
|
|
||||||
return c.json({ status: 'ok', timestamp: Date.now() });
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ export interface Config {
|
|||||||
port: number;
|
port: number;
|
||||||
storageType: 'sqlite' | 'memory';
|
storageType: 'sqlite' | 'memory';
|
||||||
storagePath: string;
|
storagePath: string;
|
||||||
sessionTimeout: number;
|
offerTimeout: number;
|
||||||
corsOrigins: string[];
|
corsOrigins: string[];
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,9 +19,10 @@ export function loadConfig(): Config {
|
|||||||
port: parseInt(process.env.PORT || '3000', 10),
|
port: parseInt(process.env.PORT || '3000', 10),
|
||||||
storageType: (process.env.STORAGE_TYPE || 'sqlite') as 'sqlite' | 'memory',
|
storageType: (process.env.STORAGE_TYPE || 'sqlite') as 'sqlite' | 'memory',
|
||||||
storagePath: process.env.STORAGE_PATH || ':memory:',
|
storagePath: process.env.STORAGE_PATH || ':memory:',
|
||||||
sessionTimeout: parseInt(process.env.SESSION_TIMEOUT || '300000', 10),
|
offerTimeout: parseInt(process.env.OFFER_TIMEOUT || '60000', 10),
|
||||||
corsOrigins: process.env.CORS_ORIGINS
|
corsOrigins: process.env.CORS_ORIGINS
|
||||||
? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
|
? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
|
||||||
: ['*'],
|
: ['*'],
|
||||||
|
version: process.env.VERSION || 'unknown',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ async function main() {
|
|||||||
port: config.port,
|
port: config.port,
|
||||||
storageType: config.storageType,
|
storageType: config.storageType,
|
||||||
storagePath: config.storagePath,
|
storagePath: config.storagePath,
|
||||||
sessionTimeout: `${config.sessionTimeout}ms`,
|
offerTimeout: `${config.offerTimeout}ms`,
|
||||||
corsOrigins: config.corsOrigins,
|
corsOrigins: config.corsOrigins,
|
||||||
|
version: config.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
let storage: Storage;
|
let storage: Storage;
|
||||||
@@ -29,9 +30,9 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = createApp(storage, {
|
const app = createApp(storage, {
|
||||||
sessionTimeout: config.sessionTimeout,
|
offerTimeout: config.offerTimeout,
|
||||||
corsOrigins: config.corsOrigins,
|
corsOrigins: config.corsOrigins,
|
||||||
version: process.env.RONDEVU_VERSION || 'unknown',
|
version: config.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = serve({
|
const server = serve({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Storage, Session } from './types.ts';
|
import { Storage, Offer } from './types.ts';
|
||||||
|
|
||||||
// Generate a UUID v4
|
// Generate a UUID v4
|
||||||
function generateUUID(): string {
|
function generateUUID(): string {
|
||||||
@@ -6,7 +6,7 @@ function generateUUID(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* D1 storage adapter for session management using Cloudflare D1
|
* D1 storage adapter for offer management using Cloudflare D1
|
||||||
*/
|
*/
|
||||||
export class D1Storage implements Storage {
|
export class D1Storage implements Storage {
|
||||||
private db: D1Database;
|
private db: D1Database;
|
||||||
@@ -25,10 +25,9 @@ export class D1Storage implements Storage {
|
|||||||
*/
|
*/
|
||||||
async initializeDatabase(): Promise<void> {
|
async initializeDatabase(): Promise<void> {
|
||||||
await this.db.exec(`
|
await this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS offers (
|
||||||
code TEXT PRIMARY KEY,
|
code TEXT PRIMARY KEY,
|
||||||
origin TEXT NOT NULL,
|
origin TEXT NOT NULL,
|
||||||
topic TEXT NOT NULL,
|
|
||||||
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
||||||
offer TEXT NOT NULL,
|
offer TEXT NOT NULL,
|
||||||
answer TEXT,
|
answer TEXT,
|
||||||
@@ -38,113 +37,13 @@ export class D1Storage implements Storage {
|
|||||||
expires_at INTEGER NOT NULL
|
expires_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_expires_at ON sessions(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_origin_topic ON sessions(origin, topic);
|
CREATE INDEX IF NOT EXISTS idx_offers_origin ON offers(origin);
|
||||||
CREATE INDEX IF NOT EXISTS idx_origin_topic_expires ON sessions(origin, topic, expires_at);
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTopics(origin: string, page: number = 1, limit: number = 100): Promise<{
|
async createOffer(
|
||||||
topics: Array<{ topic: string; count: number }>;
|
|
||||||
pagination: {
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
total: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
// Clamp limit to maximum of 1000
|
|
||||||
const effectiveLimit = Math.min(limit, 1000);
|
|
||||||
const offset = (page - 1) * effectiveLimit;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get total count of topics for this origin
|
|
||||||
const countResult = await this.db.prepare(`
|
|
||||||
SELECT COUNT(DISTINCT topic) as total
|
|
||||||
FROM sessions
|
|
||||||
WHERE origin = ? AND expires_at > ? AND answer IS NULL
|
|
||||||
`).bind(origin, Date.now()).first();
|
|
||||||
|
|
||||||
const total = countResult ? Number(countResult.total) : 0;
|
|
||||||
|
|
||||||
// Get paginated topics
|
|
||||||
const result = await this.db.prepare(`
|
|
||||||
SELECT topic, COUNT(*) as count
|
|
||||||
FROM sessions
|
|
||||||
WHERE origin = ? AND expires_at > ? AND answer IS NULL
|
|
||||||
GROUP BY topic
|
|
||||||
ORDER BY topic ASC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`).bind(origin, Date.now(), effectiveLimit, offset).all();
|
|
||||||
|
|
||||||
// D1 returns results in the results array, or empty array if no results
|
|
||||||
if (!result.results) {
|
|
||||||
console.error('[D1] listTopics: No results property in response:', result);
|
|
||||||
return {
|
|
||||||
topics: [],
|
|
||||||
pagination: {
|
|
||||||
page,
|
|
||||||
limit: effectiveLimit,
|
|
||||||
total: 0,
|
|
||||||
hasMore: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const topics = result.results.map((row: any) => ({
|
|
||||||
topic: row.topic,
|
|
||||||
count: Number(row.count),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
topics,
|
|
||||||
pagination: {
|
|
||||||
page,
|
|
||||||
limit: effectiveLimit,
|
|
||||||
total,
|
|
||||||
hasMore: offset + topics.length < total,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[D1] listTopics error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async listSessionsByTopic(origin: string, topic: string): Promise<Session[]> {
|
|
||||||
try {
|
|
||||||
const result = await this.db.prepare(`
|
|
||||||
SELECT * FROM sessions
|
|
||||||
WHERE origin = ? AND topic = ? AND expires_at > ? AND answer IS NULL
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`).bind(origin, topic, Date.now()).all();
|
|
||||||
|
|
||||||
if (!result.results) {
|
|
||||||
console.error('[D1] listSessionsByTopic: No results property in response:', result);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.results.map((row: any) => ({
|
|
||||||
code: row.code,
|
|
||||||
origin: row.origin,
|
|
||||||
topic: row.topic,
|
|
||||||
peerId: row.peer_id,
|
|
||||||
offer: row.offer,
|
|
||||||
answer: row.answer || undefined,
|
|
||||||
offerCandidates: JSON.parse(row.offer_candidates),
|
|
||||||
answerCandidates: JSON.parse(row.answer_candidates),
|
|
||||||
createdAt: row.created_at,
|
|
||||||
expiresAt: row.expires_at,
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[D1] listSessionsByTopic error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(
|
|
||||||
origin: string,
|
origin: string,
|
||||||
topic: string,
|
|
||||||
peerId: string,
|
peerId: string,
|
||||||
offer: string,
|
offer: string,
|
||||||
expiresAt: number,
|
expiresAt: number,
|
||||||
@@ -160,21 +59,21 @@ export class D1Storage implements Storage {
|
|||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
if (attempts > maxAttempts) {
|
if (attempts > maxAttempts) {
|
||||||
throw new Error('Failed to generate unique session code');
|
throw new Error('Failed to generate unique offer code');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
INSERT INTO sessions (code, origin, topic, peer_id, offer, created_at, expires_at)
|
INSERT INTO offers (code, origin, peer_id, offer, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).bind(code, origin, topic, peerId, offer, Date.now(), expiresAt).run();
|
`).bind(code, origin, peerId, offer, Date.now(), expiresAt).run();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// If unique constraint failed with custom code, throw error
|
// If unique constraint failed with custom code, throw error
|
||||||
if (err.message?.includes('UNIQUE constraint failed')) {
|
if (err.message?.includes('UNIQUE constraint failed')) {
|
||||||
if (customCode) {
|
if (customCode) {
|
||||||
throw new Error(`Session code '${customCode}' already exists`);
|
throw new Error(`Offer code '${customCode}' already exists`);
|
||||||
}
|
}
|
||||||
// Try again with new generated code
|
// Try again with new generated code
|
||||||
continue;
|
continue;
|
||||||
@@ -186,10 +85,10 @@ export class D1Storage implements Storage {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSession(code: string, origin: string): Promise<Session | null> {
|
async getOffer(code: string, origin: string): Promise<Offer | null> {
|
||||||
try {
|
try {
|
||||||
const result = await this.db.prepare(`
|
const result = await this.db.prepare(`
|
||||||
SELECT * FROM sessions
|
SELECT * FROM offers
|
||||||
WHERE code = ? AND origin = ? AND expires_at > ?
|
WHERE code = ? AND origin = ? AND expires_at > ?
|
||||||
`).bind(code, origin, Date.now()).first();
|
`).bind(code, origin, Date.now()).first();
|
||||||
|
|
||||||
@@ -202,7 +101,6 @@ export class D1Storage implements Storage {
|
|||||||
return {
|
return {
|
||||||
code: row.code,
|
code: row.code,
|
||||||
origin: row.origin,
|
origin: row.origin,
|
||||||
topic: row.topic,
|
|
||||||
peerId: row.peer_id,
|
peerId: row.peer_id,
|
||||||
offer: row.offer,
|
offer: row.offer,
|
||||||
answer: row.answer || undefined,
|
answer: row.answer || undefined,
|
||||||
@@ -212,17 +110,17 @@ export class D1Storage implements Storage {
|
|||||||
expiresAt: row.expires_at,
|
expiresAt: row.expires_at,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[D1] getSession error:', error);
|
console.error('[D1] getOffer error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSession(code: string, origin: string, update: Partial<Session>): Promise<void> {
|
async updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void> {
|
||||||
// Verify session exists and origin matches
|
// Verify offer exists and origin matches
|
||||||
const current = await this.getSession(code, origin);
|
const current = await this.getOffer(code, origin);
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Session not found or origin mismatch');
|
throw new Error('Offer not found or origin mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build update query dynamically based on what fields are being updated
|
// Build update query dynamically based on what fields are being updated
|
||||||
@@ -253,7 +151,7 @@ export class D1Storage implements Storage {
|
|||||||
|
|
||||||
// D1 provides strong consistency, so this update is atomic and immediately visible
|
// D1 provides strong consistency, so this update is atomic and immediately visible
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE sessions
|
UPDATE offers
|
||||||
SET ${updates.join(', ')}
|
SET ${updates.join(', ')}
|
||||||
WHERE code = ? AND origin = ?
|
WHERE code = ? AND origin = ?
|
||||||
`;
|
`;
|
||||||
@@ -261,22 +159,22 @@ export class D1Storage implements Storage {
|
|||||||
await this.db.prepare(query).bind(...values).run();
|
await this.db.prepare(query).bind(...values).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSession(code: string): Promise<void> {
|
async deleteOffer(code: string): Promise<void> {
|
||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
DELETE FROM sessions WHERE code = ?
|
DELETE FROM offers WHERE code = ?
|
||||||
`).bind(code).run();
|
`).bind(code).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanupExpiredSessions(): Promise<number> {
|
async cleanupExpiredOffers(): Promise<number> {
|
||||||
const result = await this.db.prepare(`
|
const result = await this.db.prepare(`
|
||||||
DELETE FROM sessions WHERE expires_at <= ?
|
DELETE FROM offers WHERE expires_at <= ?
|
||||||
`).bind(Date.now()).run();
|
`).bind(Date.now()).run();
|
||||||
|
|
||||||
return result.meta.changes || 0;
|
return result.meta.changes || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
await this.cleanupExpiredSessions();
|
await this.cleanupExpiredOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { Storage, Session } from './types.ts';
|
import { Storage, Offer } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQLite storage adapter for session management
|
* SQLite storage adapter for offer management
|
||||||
* Supports both file-based and in-memory databases
|
* Supports both file-based and in-memory databases
|
||||||
*/
|
*/
|
||||||
export class SQLiteStorage implements Storage {
|
export class SQLiteStorage implements Storage {
|
||||||
@@ -24,10 +24,9 @@ export class SQLiteStorage implements Storage {
|
|||||||
*/
|
*/
|
||||||
private initializeDatabase(): void {
|
private initializeDatabase(): void {
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS offers (
|
||||||
code TEXT PRIMARY KEY,
|
code TEXT PRIMARY KEY,
|
||||||
origin TEXT NOT NULL,
|
origin TEXT NOT NULL,
|
||||||
topic TEXT NOT NULL,
|
|
||||||
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
||||||
offer TEXT NOT NULL,
|
offer TEXT NOT NULL,
|
||||||
answer TEXT,
|
answer TEXT,
|
||||||
@@ -37,14 +36,13 @@ export class SQLiteStorage implements Storage {
|
|||||||
expires_at INTEGER NOT NULL
|
expires_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_expires_at ON sessions(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_origin_topic ON sessions(origin, topic);
|
CREATE INDEX IF NOT EXISTS idx_offers_origin ON offers(origin);
|
||||||
CREATE INDEX IF NOT EXISTS idx_origin_topic_expires ON sessions(origin, topic, expires_at);
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts periodic cleanup of expired sessions
|
* Starts periodic cleanup of expired offers
|
||||||
*/
|
*/
|
||||||
private startCleanupInterval(): void {
|
private startCleanupInterval(): void {
|
||||||
// Run cleanup every minute
|
// Run cleanup every minute
|
||||||
@@ -62,7 +60,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
return randomUUID();
|
return randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(origin: string, topic: string, peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string> {
|
async createOffer(origin: string, peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string> {
|
||||||
// Validate peerId length
|
// Validate peerId length
|
||||||
if (peerId.length > 1024) {
|
if (peerId.length > 1024) {
|
||||||
throw new Error('PeerId string must be 1024 characters or less');
|
throw new Error('PeerId string must be 1024 characters or less');
|
||||||
@@ -78,22 +76,22 @@ export class SQLiteStorage implements Storage {
|
|||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
if (attempts > maxAttempts) {
|
if (attempts > maxAttempts) {
|
||||||
throw new Error('Failed to generate unique session code');
|
throw new Error('Failed to generate unique offer code');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
INSERT INTO sessions (code, origin, topic, peer_id, offer, created_at, expires_at)
|
INSERT INTO offers (code, origin, peer_id, offer, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run(code, origin, topic, peerId, offer, Date.now(), expiresAt);
|
stmt.run(code, origin, peerId, offer, Date.now(), expiresAt);
|
||||||
break;
|
break;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// If unique constraint failed with custom code, throw error
|
// If unique constraint failed with custom code, throw error
|
||||||
if (err.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
|
if (err.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
|
||||||
if (customCode) {
|
if (customCode) {
|
||||||
throw new Error(`Session code '${customCode}' already exists`);
|
throw new Error(`Offer code '${customCode}' already exists`);
|
||||||
}
|
}
|
||||||
// Try again with new generated code
|
// Try again with new generated code
|
||||||
continue;
|
continue;
|
||||||
@@ -105,82 +103,9 @@ export class SQLiteStorage implements Storage {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listSessionsByTopic(origin: string, topic: string): Promise<Session[]> {
|
async getOffer(code: string, origin: string): Promise<Offer | null> {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT * FROM sessions
|
SELECT * FROM offers WHERE code = ? AND origin = ? AND expires_at > ?
|
||||||
WHERE origin = ? AND topic = ? AND expires_at > ? AND answer IS NULL
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`);
|
|
||||||
|
|
||||||
const rows = stmt.all(origin, topic, Date.now()) as any[];
|
|
||||||
|
|
||||||
return rows.map(row => ({
|
|
||||||
code: row.code,
|
|
||||||
origin: row.origin,
|
|
||||||
topic: row.topic,
|
|
||||||
peerId: row.peer_id,
|
|
||||||
offer: row.offer,
|
|
||||||
answer: row.answer || undefined,
|
|
||||||
offerCandidates: JSON.parse(row.offer_candidates),
|
|
||||||
answerCandidates: JSON.parse(row.answer_candidates),
|
|
||||||
createdAt: row.created_at,
|
|
||||||
expiresAt: row.expires_at,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async listTopics(origin: string, page: number, limit: number): Promise<{
|
|
||||||
topics: Array<{ topic: string; count: number }>;
|
|
||||||
pagination: {
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
total: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
// Ensure limit doesn't exceed 1000
|
|
||||||
const safeLimit = Math.min(Math.max(1, limit), 1000);
|
|
||||||
const safePage = Math.max(1, page);
|
|
||||||
const offset = (safePage - 1) * safeLimit;
|
|
||||||
|
|
||||||
// Get total count of topics
|
|
||||||
const countStmt = this.db.prepare(`
|
|
||||||
SELECT COUNT(DISTINCT topic) as total
|
|
||||||
FROM sessions
|
|
||||||
WHERE origin = ? AND expires_at > ? AND answer IS NULL
|
|
||||||
`);
|
|
||||||
const { total } = countStmt.get(origin, Date.now()) as any;
|
|
||||||
|
|
||||||
// Get paginated topics
|
|
||||||
const stmt = this.db.prepare(`
|
|
||||||
SELECT topic, COUNT(*) as count
|
|
||||||
FROM sessions
|
|
||||||
WHERE origin = ? AND expires_at > ? AND answer IS NULL
|
|
||||||
GROUP BY topic
|
|
||||||
ORDER BY topic ASC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`);
|
|
||||||
|
|
||||||
const rows = stmt.all(origin, Date.now(), safeLimit, offset) as any[];
|
|
||||||
|
|
||||||
const topics = rows.map(row => ({
|
|
||||||
topic: row.topic,
|
|
||||||
count: row.count,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
topics,
|
|
||||||
pagination: {
|
|
||||||
page: safePage,
|
|
||||||
limit: safeLimit,
|
|
||||||
total,
|
|
||||||
hasMore: offset + topics.length < total,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSession(code: string, origin: string): Promise<Session | null> {
|
|
||||||
const stmt = this.db.prepare(`
|
|
||||||
SELECT * FROM sessions WHERE code = ? AND origin = ? AND expires_at > ?
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const row = stmt.get(code, origin, Date.now()) as any;
|
const row = stmt.get(code, origin, Date.now()) as any;
|
||||||
@@ -192,7 +117,6 @@ export class SQLiteStorage implements Storage {
|
|||||||
return {
|
return {
|
||||||
code: row.code,
|
code: row.code,
|
||||||
origin: row.origin,
|
origin: row.origin,
|
||||||
topic: row.topic,
|
|
||||||
peerId: row.peer_id,
|
peerId: row.peer_id,
|
||||||
offer: row.offer,
|
offer: row.offer,
|
||||||
answer: row.answer || undefined,
|
answer: row.answer || undefined,
|
||||||
@@ -203,11 +127,11 @@ export class SQLiteStorage implements Storage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSession(code: string, origin: string, update: Partial<Session>): Promise<void> {
|
async updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void> {
|
||||||
const current = await this.getSession(code, origin);
|
const current = await this.getOffer(code, origin);
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Session not found or origin mismatch');
|
throw new Error('Offer not found or origin mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates: string[] = [];
|
const updates: string[] = [];
|
||||||
@@ -236,23 +160,23 @@ export class SQLiteStorage implements Storage {
|
|||||||
values.push(origin);
|
values.push(origin);
|
||||||
|
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
UPDATE sessions SET ${updates.join(', ')} WHERE code = ? AND origin = ?
|
UPDATE offers SET ${updates.join(', ')} WHERE code = ? AND origin = ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run(...values);
|
stmt.run(...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSession(code: string): Promise<void> {
|
async deleteOffer(code: string): Promise<void> {
|
||||||
const stmt = this.db.prepare('DELETE FROM sessions WHERE code = ?');
|
const stmt = this.db.prepare('DELETE FROM offers WHERE code = ?');
|
||||||
stmt.run(code);
|
stmt.run(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
const stmt = this.db.prepare('DELETE FROM sessions WHERE expires_at <= ?');
|
const stmt = this.db.prepare('DELETE FROM offers WHERE expires_at <= ?');
|
||||||
const result = stmt.run(Date.now());
|
const result = stmt.run(Date.now());
|
||||||
|
|
||||||
if (result.changes > 0) {
|
if (result.changes > 0) {
|
||||||
console.log(`Cleaned up ${result.changes} expired session(s)`);
|
console.log(`Cleaned up ${result.changes} expired offer(s)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Represents a WebRTC signaling session
|
* Represents a WebRTC signaling offer
|
||||||
*/
|
*/
|
||||||
export interface Session {
|
export interface Offer {
|
||||||
code: string;
|
code: string;
|
||||||
origin: string;
|
origin: string;
|
||||||
topic: string;
|
|
||||||
peerId: string;
|
peerId: string;
|
||||||
offer: string;
|
offer: string;
|
||||||
answer?: string;
|
answer?: string;
|
||||||
@@ -15,71 +14,45 @@ export interface Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage interface for session management
|
* Storage interface for offer management
|
||||||
* Implementations can use different backends (SQLite, Redis, Memory, etc.)
|
* Implementations can use different backends (SQLite, D1, Memory, etc.)
|
||||||
*/
|
*/
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
/**
|
/**
|
||||||
* Creates a new session with the given offer
|
* Creates a new offer
|
||||||
* @param origin The Origin header from the request
|
* @param origin The Origin header from the request
|
||||||
* @param topic The topic to post the offer to
|
|
||||||
* @param peerId Peer identifier string (max 1024 chars)
|
* @param peerId Peer identifier string (max 1024 chars)
|
||||||
* @param offer The WebRTC SDP offer message
|
* @param offer The WebRTC SDP offer message
|
||||||
* @param expiresAt Unix timestamp when the session should expire
|
* @param expiresAt Unix timestamp when the offer should expire
|
||||||
* @param customCode Optional custom code (if not provided, generates UUID)
|
* @param customCode Optional custom code (if not provided, generates UUID)
|
||||||
* @returns The unique session code
|
* @returns The unique offer code
|
||||||
*/
|
*/
|
||||||
createSession(origin: string, topic: string, peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string>;
|
createOffer(origin: string, peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all unanswered sessions for a given origin and topic
|
* Retrieves an offer by its code
|
||||||
* @param origin The Origin header from the request
|
* @param code The offer code
|
||||||
* @param topic The topic to list offers for
|
|
||||||
* @returns Array of sessions that haven't been answered yet
|
|
||||||
*/
|
|
||||||
listSessionsByTopic(origin: string, topic: string): Promise<Session[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists all topics for a given origin with their session counts
|
|
||||||
* @param origin The Origin header from the request
|
|
||||||
* @param page Page number (starting from 1)
|
|
||||||
* @param limit Number of results per page (max 1000)
|
|
||||||
* @returns Object with topics array and pagination metadata
|
|
||||||
*/
|
|
||||||
listTopics(origin: string, page: number, limit: number): Promise<{
|
|
||||||
topics: Array<{ topic: string; count: number }>;
|
|
||||||
pagination: {
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
total: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a session by its code
|
|
||||||
* @param code The session code
|
|
||||||
* @param origin The Origin header from the request (for validation)
|
* @param origin The Origin header from the request (for validation)
|
||||||
* @returns The session if found, null otherwise
|
* @returns The offer if found, null otherwise
|
||||||
*/
|
*/
|
||||||
getSession(code: string, origin: string): Promise<Session | null>;
|
getOffer(code: string, origin: string): Promise<Offer | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing session with new data
|
* Updates an existing offer with new data
|
||||||
* @param code The session code
|
* @param code The offer code
|
||||||
* @param origin The Origin header from the request (for validation)
|
* @param origin The Origin header from the request (for validation)
|
||||||
* @param update Partial session data to update
|
* @param update Partial offer data to update
|
||||||
*/
|
*/
|
||||||
updateSession(code: string, origin: string, update: Partial<Session>): Promise<void>;
|
updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a session
|
* Deletes an offer
|
||||||
* @param code The session code
|
* @param code The offer code
|
||||||
*/
|
*/
|
||||||
deleteSession(code: string): Promise<void>;
|
deleteOffer(code: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes expired sessions
|
* Removes expired offers
|
||||||
* Should be called periodically to clean up old data
|
* Should be called periodically to clean up old data
|
||||||
*/
|
*/
|
||||||
cleanup(): Promise<void>;
|
cleanup(): Promise<void>;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { D1Storage } from './storage/d1.ts';
|
|||||||
*/
|
*/
|
||||||
export interface Env {
|
export interface Env {
|
||||||
DB: D1Database;
|
DB: D1Database;
|
||||||
SESSION_TIMEOUT?: string;
|
OFFER_TIMEOUT?: string;
|
||||||
CORS_ORIGINS?: string;
|
CORS_ORIGINS?: string;
|
||||||
VERSION?: string;
|
VERSION?: string;
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,9 @@ export default {
|
|||||||
const storage = new D1Storage(env.DB);
|
const storage = new D1Storage(env.DB);
|
||||||
|
|
||||||
// Parse configuration
|
// Parse configuration
|
||||||
const sessionTimeout = env.SESSION_TIMEOUT
|
const offerTimeout = env.OFFER_TIMEOUT
|
||||||
? parseInt(env.SESSION_TIMEOUT, 10)
|
? parseInt(env.OFFER_TIMEOUT, 10)
|
||||||
: 300000; // 5 minutes default
|
: 60000; // 1 minute default
|
||||||
|
|
||||||
const corsOrigins = env.CORS_ORIGINS
|
const corsOrigins = env.CORS_ORIGINS
|
||||||
? env.CORS_ORIGINS.split(',').map(o => o.trim())
|
? env.CORS_ORIGINS.split(',').map(o => o.trim())
|
||||||
@@ -30,7 +30,7 @@ export default {
|
|||||||
|
|
||||||
// Create Hono app
|
// Create Hono app
|
||||||
const app = createApp(storage, {
|
const app = createApp(storage, {
|
||||||
sessionTimeout,
|
offerTimeout,
|
||||||
corsOrigins,
|
corsOrigins,
|
||||||
version: env.VERSION || 'unknown',
|
version: env.VERSION || 'unknown',
|
||||||
});
|
});
|
||||||
@@ -41,19 +41,19 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Scheduled handler for cron triggers
|
* Scheduled handler for cron triggers
|
||||||
* Runs every 5 minutes to clean up expired sessions
|
* Runs every minute to clean up expired offers
|
||||||
*/
|
*/
|
||||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||||
const storage = new D1Storage(env.DB);
|
const storage = new D1Storage(env.DB);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete expired sessions using the storage method
|
// Delete expired offers using the storage method
|
||||||
const deletedCount = await storage.cleanupExpiredSessions();
|
const deletedCount = await storage.cleanupExpiredOffers();
|
||||||
|
|
||||||
console.log(`Cleaned up ${deletedCount} expired sessions at ${new Date(now).toISOString()}`);
|
console.log(`Cleaned up ${deletedCount} expired offers at ${new Date(now).toISOString()}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cleaning up sessions:', error);
|
console.error('Error cleaning up offers:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ compatibility_date = "2024-01-01"
|
|||||||
# D1 Database binding
|
# D1 Database binding
|
||||||
[[d1_databases]]
|
[[d1_databases]]
|
||||||
binding = "DB"
|
binding = "DB"
|
||||||
database_name = "rondevu-sessions"
|
database_name = "rondevu-offers"
|
||||||
database_id = "b94e3f71-816d-455b-a89d-927fa49532d0"
|
database_id = "b94e3f71-816d-455b-a89d-927fa49532d0"
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
[vars]
|
[vars]
|
||||||
SESSION_TIMEOUT = "60000" # 1 minute in milliseconds
|
OFFER_TIMEOUT = "60000" # 1 minute in milliseconds
|
||||||
CORS_ORIGINS = "*" # Comma-separated list of allowed origins
|
CORS_ORIGINS = "*" # Comma-separated list of allowed origins
|
||||||
VERSION = "unknown" # Set to git commit hash before deploying
|
VERSION = "0.0.1" # Semantic version
|
||||||
|
|
||||||
# Build configuration
|
# Build configuration
|
||||||
[build]
|
[build]
|
||||||
|
|||||||
Reference in New Issue
Block a user