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:
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user