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

View File

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

View File

@@ -16,18 +16,27 @@ export interface Offer {
/**
* Represents an ICE candidate for WebRTC signaling
* Stores the complete RTCIceCandidateInit object
*/
export interface IceCandidate {
id: number;
offerId: string;
peerId: string;
role: 'offerer' | 'answerer';
candidate: string;
sdpMid: string | null;
sdpMLineIndex: number | null;
candidate: RTCIceCandidateInit; // Full candidate object as JSON
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
*/
@@ -127,18 +136,14 @@ export interface Storage {
* @param offerId Offer identifier
* @param peerId Peer ID posting the candidates
* @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
*/
addIceCandidates(
offerId: string,
peerId: string,
role: 'offerer' | 'answerer',
candidates: Array<{
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
candidates: RTCIceCandidateInit[]
): Promise<number>;
/**