Refactor: Store ICE candidates as JSON objects

- Simplify storage by storing entire RTCIceCandidateInit as JSON
- Remove individual sdp_mid and sdp_m_line_index columns
- More future-proof and maintainable approach
- Recreated ice_candidates table in D1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-14 19:38:40 +01:00
parent b3bd2679fc
commit f4b2dfdb94
3 changed files with 29 additions and 44 deletions

View File

@@ -1,4 +1,4 @@
import { Storage, Offer, IceCandidate, CreateOfferRequest, TopicInfo } from './types.ts'; import { Storage, Offer, IceCandidate, CreateOfferRequest, TopicInfo, RTCIceCandidateInit } from './types.ts';
// Generate a UUID v4 // Generate a UUID v4
function generateUUID(): string { function generateUUID(): string {
@@ -58,9 +58,7 @@ export class D1Storage implements Storage {
offer_id TEXT NOT NULL, offer_id TEXT NOT NULL,
peer_id TEXT NOT NULL, peer_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')), role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
candidate TEXT NOT NULL, candidate TEXT NOT NULL, -- JSON: RTCIceCandidateInit object
sdp_mid TEXT,
sdp_m_line_index INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
); );
@@ -244,24 +242,18 @@ export class D1Storage implements Storage {
offerId: string, offerId: string,
peerId: string, peerId: string,
role: 'offerer' | 'answerer', role: 'offerer' | 'answerer',
candidates: Array<{ candidates: RTCIceCandidateInit[]
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<number> { ): Promise<number> {
// D1 doesn't have transactions, so insert one by one // D1 doesn't have transactions, so insert one by one
for (const cand of candidates) { for (const cand of candidates) {
await this.db.prepare(` await this.db.prepare(`
INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, sdp_mid, sdp_m_line_index, created_at) INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`).bind( `).bind(
offerId, offerId,
peerId, peerId,
role, role,
cand.candidate, JSON.stringify(cand), // Store full object as JSON
cand.sdpMid ?? null,
cand.sdpMLineIndex ?? null,
Date.now() Date.now()
).run(); ).run();
} }
@@ -299,9 +291,7 @@ export class D1Storage implements Storage {
offerId: row.offer_id, offerId: row.offer_id,
peerId: row.peer_id, peerId: row.peer_id,
role: row.role, role: row.role,
candidate: row.candidate, candidate: JSON.parse(row.candidate), // Parse JSON back to object
sdpMid: row.sdp_mid,
sdpMLineIndex: row.sdp_m_line_index,
createdAt: row.created_at, createdAt: row.created_at,
})); }));
} }

View File

@@ -1,6 +1,6 @@
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { Storage, Offer, IceCandidate, CreateOfferRequest, TopicInfo } from './types.ts'; import { Storage, Offer, IceCandidate, CreateOfferRequest, TopicInfo, RTCIceCandidateInit } from './types.ts';
/** /**
* SQLite storage adapter for topic-based offer management * SQLite storage adapter for topic-based offer management
@@ -55,9 +55,7 @@ export class SQLiteStorage implements Storage {
offer_id TEXT NOT NULL, offer_id TEXT NOT NULL,
peer_id TEXT NOT NULL, peer_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')), role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
candidate TEXT NOT NULL, candidate TEXT NOT NULL, -- JSON: RTCIceCandidateInit object
sdp_mid TEXT,
sdp_m_line_index INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
); );
@@ -254,26 +252,20 @@ export class SQLiteStorage implements Storage {
offerId: string, offerId: string,
peerId: string, peerId: string,
role: 'offerer' | 'answerer', role: 'offerer' | 'answerer',
candidates: Array<{ candidates: RTCIceCandidateInit[]
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<number> { ): Promise<number> {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, sdp_mid, sdp_m_line_index, created_at) INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`); `);
const transaction = this.db.transaction((candidates: typeof candidates) => { const transaction = this.db.transaction((candidates: RTCIceCandidateInit[]) => {
for (const cand of candidates) { for (const cand of candidates) {
stmt.run( stmt.run(
offerId, offerId,
peerId, peerId,
role, role,
cand.candidate, JSON.stringify(cand), // Store full object as JSON
cand.sdpMid ?? null,
cand.sdpMLineIndex ?? null,
Date.now() Date.now()
); );
} }
@@ -310,9 +302,7 @@ export class SQLiteStorage implements Storage {
offerId: row.offer_id, offerId: row.offer_id,
peerId: row.peer_id, peerId: row.peer_id,
role: row.role, role: row.role,
candidate: row.candidate, candidate: JSON.parse(row.candidate), // Parse JSON back to object
sdpMid: row.sdp_mid,
sdpMLineIndex: row.sdp_m_line_index,
createdAt: row.created_at, createdAt: row.created_at,
})); }));
} }

View File

@@ -16,18 +16,27 @@ export interface Offer {
/** /**
* Represents an ICE candidate for WebRTC signaling * Represents an ICE candidate for WebRTC signaling
* Stores the complete RTCIceCandidateInit object
*/ */
export interface IceCandidate { export interface IceCandidate {
id: number; id: number;
offerId: string; offerId: string;
peerId: string; peerId: string;
role: 'offerer' | 'answerer'; role: 'offerer' | 'answerer';
candidate: string; candidate: RTCIceCandidateInit; // Full candidate object as JSON
sdpMid: string | null;
sdpMLineIndex: number | null;
createdAt: number; createdAt: number;
} }
/**
* RTCIceCandidateInit interface for TypeScript environments without WebRTC
*/
export interface RTCIceCandidateInit {
candidate?: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
usernameFragment?: string | null;
}
/** /**
* Represents a topic with active peer count * Represents a topic with active peer count
*/ */
@@ -127,18 +136,14 @@ export interface Storage {
* @param offerId Offer identifier * @param offerId Offer identifier
* @param peerId Peer ID posting the candidates * @param peerId Peer ID posting the candidates
* @param role Role of the peer (offerer or answerer) * @param role Role of the peer (offerer or answerer)
* @param candidates Array of ICE candidate objects * @param candidates Array of RTCIceCandidateInit objects
* @returns Number of candidates added * @returns Number of candidates added
*/ */
addIceCandidates( addIceCandidates(
offerId: string, offerId: string,
peerId: string, peerId: string,
role: 'offerer' | 'answerer', role: 'offerer' | 'answerer',
candidates: Array<{ candidates: RTCIceCandidateInit[]
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<number>; ): Promise<number>;
/** /**