fix(tsconnect): move IndexedDBState and use IpnClient in helpers

Place IndexedDBState in the state-storage section alongside
InMemoryState and WebStorageState rather than between the
getIceServers doc comment and its function body.

Change getIceServers to accept IpnClient instead of the raw IPN
interface so callers can pass either IPN or IpnWorkerClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 12:56:37 +00:00
co-authored by Claude
parent e29a12cc9b
commit 34c35fa91a
+54 -53
View File
@@ -1,4 +1,5 @@
import type { DERPMap, IPN, IPNStateStorage, UserIPNFileOps } from "./types.js"
import type { DERPMap, IPNStateStorage, UserIPNFileOps } from "./types.js"
import type { IpnClient } from "./interface.js"
export class InMemoryFileOps implements UserIPNFileOps {
#map: Map<string, Uint8Array[]>
@@ -129,6 +130,57 @@ export class WebStorageState implements IPNStateStorage {
}
}
/** IPN state storage backed by IndexedDB. Loads all keys into memory at startup
* for synchronous reads; writes through to IDB asynchronously. */
export class IndexedDBState implements IPNStateStorage {
readonly #db: IDBDatabase
readonly #storeName: string
readonly #cache: Map<string, string>
private constructor(db: IDBDatabase, storeName: string, cache: Map<string, string>) {
this.#db = db
this.#storeName = storeName
this.#cache = cache
}
static open(dbName = "tsconnect-state", storeName = "state"): Promise<IndexedDBState> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(dbName, 1)
req.onupgradeneeded = () => {
req.result.createObjectStore(storeName)
}
req.onsuccess = () => {
const db = req.result
const cache = new Map<string, string>()
const tx = db.transaction([storeName], "readonly")
const cursorReq = tx.objectStore(storeName).openCursor()
cursorReq.onsuccess = () => {
const c = cursorReq.result
if (c) {
cache.set(c.key as string, c.value as string)
c.continue()
} else {
resolve(new IndexedDBState(db, storeName, cache))
}
}
cursorReq.onerror = () => reject(cursorReq.error)
}
req.onerror = () => reject(req.error)
req.onblocked = () => reject(new Error("IndexedDB open blocked"))
})
}
getState(id: string): string {
return this.#cache.get(id) ?? ""
}
setState(id: string, value: string): void {
this.#cache.set(id, value)
const tx = this.#db.transaction([this.#storeName], "readwrite")
tx.objectStore(this.#storeName).put(value, id)
}
}
/**
* Taildrop storage backend backed by the File System Access API.
* Works with any `FileSystemDirectoryHandle`; use {@link FsaFileOps.createFromOpfs}
@@ -232,58 +284,7 @@ export class FsaFileOps implements UserIPNFileOps {
* Note: in the browser, `ipn.dial()` / `ipn.listen()` always relay through
* DERP — WebRTC is the only way to achieve a direct UDP path between peers.
*/
/** IPN state storage backed by IndexedDB. Loads all keys into memory at startup
* for synchronous reads; writes through to IDB asynchronously. */
export class IndexedDBState implements IPNStateStorage {
readonly #db: IDBDatabase
readonly #storeName: string
readonly #cache: Map<string, string>
private constructor(db: IDBDatabase, storeName: string, cache: Map<string, string>) {
this.#db = db
this.#storeName = storeName
this.#cache = cache
}
static open(dbName = "tsconnect-state", storeName = "state"): Promise<IndexedDBState> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(dbName, 1)
req.onupgradeneeded = () => {
req.result.createObjectStore(storeName)
}
req.onsuccess = () => {
const db = req.result
const cache = new Map<string, string>()
const tx = db.transaction([storeName], "readonly")
const cursorReq = tx.objectStore(storeName).openCursor()
cursorReq.onsuccess = () => {
const c = cursorReq.result
if (c) {
cache.set(c.key as string, c.value as string)
c.continue()
} else {
resolve(new IndexedDBState(db, storeName, cache))
}
}
cursorReq.onerror = () => reject(cursorReq.error)
}
req.onerror = () => reject(req.error)
req.onblocked = () => reject(new Error("IndexedDB open blocked"))
})
}
getState(id: string): string {
return this.#cache.get(id) ?? ""
}
setState(id: string, value: string): void {
this.#cache.set(id, value)
const tx = this.#db.transaction([this.#storeName], "readwrite")
tx.objectStore(this.#storeName).put(value, id)
}
}
export async function getIceServers(ipn: IPN): Promise<RTCIceServer[]> {
export async function getIceServers(ipn: IpnClient): Promise<RTCIceServer[]> {
const result = await ipn.localAPI("GET", "/localapi/v0/derpmap")
if (result.status !== 200) {
throw new Error(`localapi derpmap: ${result.status} ${result.body}`)