convert XFTPClientAgent interface to XFTPAgent class

This commit is contained in:
shum
2026-02-19 07:50:15 +00:00
parent 8519a745bd
commit 4df6a2c47f
7 changed files with 57 additions and 59 deletions
+3 -3
View File
@@ -12,14 +12,14 @@ npm install xftp-web
```typescript
import {
newXFTPAgent, closeXFTPAgent,
XFTPAgent,
parseXFTPServer,
sendFile, receiveFile, deleteFile,
XFTPRetriableError, XFTPPermanentError, isRetriable,
} from "xftp-web"
// Create agent (manages connections)
const agent = newXFTPAgent()
const agent = new XFTPAgent()
const servers = [
parseXFTPServer("xftp://server1..."),
@@ -47,7 +47,7 @@ const {header, content} = await receiveFile(agent, uri)
await deleteFile(agent, sndDescription)
// Cleanup
closeXFTPAgent(agent)
agent.close()
```
### Advanced usage
+13 -13
View File
@@ -19,9 +19,9 @@ import {
import type {FileInfo} from "./protocol/commands.js"
import {
createXFTPChunk, addXFTPRecipients, uploadXFTPChunk, downloadXFTPChunk, downloadXFTPChunkRaw,
deleteXFTPChunk, ackXFTPChunk, type XFTPClientAgent
deleteXFTPChunk, ackXFTPChunk, XFTPAgent
} from "./client.js"
export {newXFTPAgent, closeXFTPAgent, type XFTPClientAgent, type TransportConfig,
export {XFTPAgent, type TransportConfig,
XFTPRetriableError, XFTPPermanentError, isRetriable, categorizeError, humanReadableMessage,
ackXFTPChunk, addXFTPRecipients} from "./client.js"
import {processDownloadedFile, decryptReceivedChunk} from "./download.js"
@@ -130,7 +130,7 @@ export interface UploadOptions {
}
async function uploadSingleChunk(
agent: XFTPClientAgent, server: XFTPServer,
agent: XFTPAgent, server: XFTPServer,
chunkNo: number, chunkData: Uint8Array, chunkSize: number,
numRecipients: number, auth: Uint8Array | null
): Promise<SentChunk> {
@@ -167,7 +167,7 @@ async function uploadSingleChunk(
}
export async function uploadFile(
agent: XFTPClientAgent,
agent: XFTPAgent,
servers: XFTPServer[],
encrypted: EncryptedFileMetadata,
options?: UploadOptions
@@ -230,17 +230,17 @@ export interface SendFileOptions {
}
export async function sendFile(
agent: XFTPClientAgent, servers: XFTPServer[],
agent: XFTPAgent, servers: XFTPServer[],
source: Uint8Array, fileName: string,
options?: SendFileOptions
): Promise<UploadResult>
export async function sendFile(
agent: XFTPClientAgent, servers: XFTPServer[],
agent: XFTPAgent, servers: XFTPServer[],
source: AsyncIterable<Uint8Array>, sourceSize: number,
fileName: string, options?: SendFileOptions
): Promise<UploadResult>
export async function sendFile(
agent: XFTPClientAgent, servers: XFTPServer[],
agent: XFTPAgent, servers: XFTPServer[],
source: Uint8Array | AsyncIterable<Uint8Array>,
fileNameOrSize: string | number,
fileNameOrOptions?: string | SendFileOptions,
@@ -362,7 +362,7 @@ function buildDescription(
}
async function uploadRedirectDescription(
agent: XFTPClientAgent,
agent: XFTPAgent,
servers: XFTPServer[],
innerFd: FileDescription,
auth?: Uint8Array
@@ -429,7 +429,7 @@ export interface DownloadRawOptions {
}
export async function downloadFileRaw(
agent: XFTPClientAgent,
agent: XFTPAgent,
fd: FileDescription,
onRawChunk: (chunk: RawDownloadedChunk) => Promise<void>,
options?: DownloadRawOptions
@@ -477,7 +477,7 @@ export async function downloadFileRaw(
}
export async function downloadFile(
agent: XFTPClientAgent,
agent: XFTPAgent,
fd: FileDescription,
onProgress?: (downloaded: number, total: number) => void
): Promise<DownloadResult> {
@@ -495,7 +495,7 @@ export async function downloadFile(
}
export async function receiveFile(
agent: XFTPClientAgent,
agent: XFTPAgent,
uri: string,
options?: {onProgress?: (downloaded: number, total: number) => void}
): Promise<DownloadResult> {
@@ -504,7 +504,7 @@ export async function receiveFile(
}
async function resolveRedirect(
agent: XFTPClientAgent,
agent: XFTPAgent,
fd: FileDescription
): Promise<FileDescription> {
const plaintextChunks: Uint8Array[] = new Array(fd.chunks.length)
@@ -542,7 +542,7 @@ async function resolveRedirect(
// -- Delete
export async function deleteFile(agent: XFTPClientAgent, sndDescription: FileDescription): Promise<void> {
export async function deleteFile(agent: XFTPAgent, sndDescription: FileDescription): Promise<void> {
const byServer = new Map<string, typeof sndDescription.chunks>()
for (const chunk of sndDescription.chunks) {
const srv = chunk.replicas[0]?.server ?? ""
+26 -26
View File
@@ -172,17 +172,24 @@ interface ServerConnection {
queue: Promise<void> // tail of sequential command chain
}
export interface XFTPClientAgent {
connections: Map<string, ServerConnection>
export class XFTPAgent {
connections = new Map<string, ServerConnection>()
/** @internal Injectable for testing — defaults to connectXFTP */
_connectFn: (server: XFTPServer) => Promise<XFTPClient>
constructor(connectFn?: (server: XFTPServer) => Promise<XFTPClient>) {
this._connectFn = connectFn ?? connectXFTP
}
close(): void {
for (const conn of this.connections.values()) {
conn.client.then(c => c.transport.close(), () => {})
}
this.connections.clear()
}
}
export function newXFTPAgent(): XFTPClientAgent {
return {connections: new Map(), _connectFn: connectXFTP}
}
export function getXFTPServerClient(agent: XFTPClientAgent, server: XFTPServer): Promise<XFTPClient> {
export function getXFTPServerClient(agent: XFTPAgent, server: XFTPServer): Promise<XFTPClient> {
const key = formatXFTPServer(server)
let conn = agent.connections.get(key)
if (!conn) {
@@ -197,7 +204,7 @@ export function getXFTPServerClient(agent: XFTPClientAgent, server: XFTPServer):
return conn.client
}
export function reconnectClient(agent: XFTPClientAgent, server: XFTPServer): Promise<XFTPClient> {
export function reconnectClient(agent: XFTPAgent, server: XFTPServer): Promise<XFTPClient> {
const key = formatXFTPServer(server)
const old = agent.connections.get(key)
old?.client.then(c => c.transport.close(), () => {})
@@ -212,7 +219,7 @@ export function reconnectClient(agent: XFTPClientAgent, server: XFTPServer): Pro
}
export function removeStaleConnection(
agent: XFTPClientAgent, server: XFTPServer, failedP: Promise<XFTPClient>
agent: XFTPAgent, server: XFTPServer, failedP: Promise<XFTPClient>
): void {
const key = formatXFTPServer(server)
const conn = agent.connections.get(key)
@@ -222,7 +229,7 @@ export function removeStaleConnection(
}
}
export function closeXFTPServerClient(agent: XFTPClientAgent, server: XFTPServer): void {
export function closeXFTPServerClient(agent: XFTPAgent, server: XFTPServer): void {
const key = formatXFTPServer(server)
const conn = agent.connections.get(key)
if (conn) {
@@ -231,13 +238,6 @@ export function closeXFTPServerClient(agent: XFTPClientAgent, server: XFTPServer
}
}
export function closeXFTPAgent(agent: XFTPClientAgent): void {
for (const conn of agent.connections.values()) {
conn.client.then(c => c.transport.close(), () => {})
}
agent.connections.clear()
}
// -- Connect + handshake
export async function connectXFTP(server: XFTPServer, config?: Partial<TransportConfig>): Promise<XFTPClient> {
@@ -342,7 +342,7 @@ function _hex(b: Uint8Array, n = 8): string {
// -- Send command (with retry + reconnect)
export async function sendXFTPCommand(
agent: XFTPClientAgent,
agent: XFTPAgent,
server: XFTPServer,
privateKey: Uint8Array,
entityId: Uint8Array,
@@ -375,7 +375,7 @@ export async function sendXFTPCommand(
// -- Command wrappers
export async function createXFTPChunk(
agent: XFTPClientAgent, server: XFTPServer, spKey: Uint8Array, file: FileInfo,
agent: XFTPAgent, server: XFTPServer, spKey: Uint8Array, file: FileInfo,
rcvKeys: Uint8Array[], auth: Uint8Array | null = null
): Promise<{senderId: Uint8Array, recipientIds: Uint8Array[]}> {
const {response} = await sendXFTPCommand(agent, server, spKey, new Uint8Array(0), encodeFNEW(file, rcvKeys, auth))
@@ -384,7 +384,7 @@ export async function createXFTPChunk(
}
export async function addXFTPRecipients(
agent: XFTPClientAgent, server: XFTPServer, spKey: Uint8Array, fId: Uint8Array, rcvKeys: Uint8Array[]
agent: XFTPAgent, server: XFTPServer, spKey: Uint8Array, fId: Uint8Array, rcvKeys: Uint8Array[]
): Promise<Uint8Array[]> {
const {response} = await sendXFTPCommand(agent, server, spKey, fId, encodeFADD(rcvKeys))
if (response.type !== "FRRcvIds") throw new Error("unexpected response: " + response.type)
@@ -392,7 +392,7 @@ export async function addXFTPRecipients(
}
export async function uploadXFTPChunk(
agent: XFTPClientAgent, server: XFTPServer, spKey: Uint8Array, fId: Uint8Array, chunkData: Uint8Array
agent: XFTPAgent, server: XFTPServer, spKey: Uint8Array, fId: Uint8Array, chunkData: Uint8Array
): Promise<void> {
const {response} = await sendXFTPCommand(agent, server, spKey, fId, encodeFPUT(), chunkData)
if (response.type !== "FROk") throw new Error("unexpected response: " + response.type)
@@ -405,7 +405,7 @@ export interface RawChunkResponse {
}
export async function downloadXFTPChunkRaw(
agent: XFTPClientAgent, server: XFTPServer, rpKey: Uint8Array, fId: Uint8Array
agent: XFTPAgent, server: XFTPServer, rpKey: Uint8Array, fId: Uint8Array
): Promise<RawChunkResponse> {
const {publicKey, privateKey} = generateX25519KeyPair()
const cmd = encodeFGET(encodePubKeyX25519(publicKey))
@@ -417,27 +417,27 @@ export async function downloadXFTPChunkRaw(
}
export async function downloadXFTPChunk(
agent: XFTPClientAgent, server: XFTPServer, rpKey: Uint8Array, fId: Uint8Array, digest?: Uint8Array
agent: XFTPAgent, server: XFTPServer, rpKey: Uint8Array, fId: Uint8Array, digest?: Uint8Array
): Promise<Uint8Array> {
const {dhSecret, nonce, body} = await downloadXFTPChunkRaw(agent, server, rpKey, fId)
return decryptReceivedChunk(dhSecret, nonce, body, digest ?? null)
}
export async function deleteXFTPChunk(
agent: XFTPClientAgent, server: XFTPServer, spKey: Uint8Array, sId: Uint8Array
agent: XFTPAgent, server: XFTPServer, spKey: Uint8Array, sId: Uint8Array
): Promise<void> {
const {response} = await sendXFTPCommand(agent, server, spKey, sId, encodeFDEL())
if (response.type !== "FROk") throw new Error("unexpected response: " + response.type)
}
export async function ackXFTPChunk(
agent: XFTPClientAgent, server: XFTPServer, rpKey: Uint8Array, rId: Uint8Array
agent: XFTPAgent, server: XFTPServer, rpKey: Uint8Array, rId: Uint8Array
): Promise<void> {
const {response} = await sendXFTPCommand(agent, server, rpKey, rId, encodeFACK())
if (response.type !== "FROk") throw new Error("unexpected response: " + response.type)
}
export async function pingXFTP(agent: XFTPClientAgent, server: XFTPServer): Promise<void> {
export async function pingXFTP(agent: XFTPAgent, server: XFTPServer): Promise<void> {
const client = await getXFTPServerClient(agent, server)
const corrId = new Uint8Array(0)
const block = encodeTransmission(client.sessionId, corrId, new Uint8Array(0), encodePING())
+3 -3
View File
@@ -1,11 +1,11 @@
import {test, expect} from 'vitest'
import {encryptFileForUpload, uploadFile, downloadFile, newXFTPAgent, closeXFTPAgent} from '../src/agent.js'
import {encryptFileForUpload, uploadFile, downloadFile, XFTPAgent} from '../src/agent.js'
import {parseXFTPServer} from '../src/protocol/address.js'
const server = parseXFTPServer(import.meta.env.XFTP_SERVER)
test('browser upload + download round-trip', async () => {
const agent = newXFTPAgent()
const agent = new XFTPAgent()
try {
const data = new Uint8Array(50000)
crypto.getRandomValues(data)
@@ -14,6 +14,6 @@ test('browser upload + download round-trip', async () => {
const {content} = await downloadFile(agent, rcvDescription)
expect(content).toEqual(data)
} finally {
closeXFTPAgent(agent)
agent.close()
}
})
+5 -7
View File
@@ -1,9 +1,9 @@
import {test, expect, vi, beforeEach} from 'vitest'
import {
newXFTPAgent, getXFTPServerClient, reconnectClient, removeStaleConnection,
XFTPAgent, getXFTPServerClient, reconnectClient, removeStaleConnection,
sendXFTPCommand,
XFTPRetriableError, XFTPPermanentError,
type XFTPClient, type XFTPClientAgent
type XFTPClient
} from '../src/client.js'
import {formatXFTPServer, type XFTPServer} from '../src/protocol/address.js'
import {blockPad} from '../src/protocol/transmission.js'
@@ -26,10 +26,8 @@ function makeMockClient(overrides?: Partial<XFTPClient>): XFTPClient {
}
}
function makeAgent(connectFn: (s: any) => Promise<XFTPClient>): XFTPClientAgent {
const agent = newXFTPAgent()
agent._connectFn = connectFn
return agent
function makeAgent(connectFn: (s: any) => Promise<XFTPClient>): XFTPAgent {
return new XFTPAgent(connectFn)
}
// T4: getXFTPServerClient coalesces concurrent calls
@@ -66,7 +64,7 @@ test('getXFTPServerClient auto-cleans failed connections', async () => {
// T6: removeStaleConnection respects promise identity
test('removeStaleConnection respects promise identity', () => {
const agent = newXFTPAgent()
const agent = new XFTPAgent()
const mockClient1 = makeMockClient()
const mockClient2 = makeMockClient()
const p1 = Promise.resolve(mockClient1)
+3 -3
View File
@@ -1,7 +1,7 @@
import {createCryptoBackend} from './crypto-backend.js'
import {createProgressRing} from './progress.js'
import {
newXFTPAgent, closeXFTPAgent,
XFTPAgent,
decodeDescriptionURI, downloadFileRaw
} from '../src/agent.js'
import {XFTPPermanentError} from '../src/client.js'
@@ -68,7 +68,7 @@ export function initDownload(app: HTMLElement, hash: string) {
statusText.textContent = 'Downloading…'
const backend = createCryptoBackend()
const agent = newXFTPAgent()
const agent = new XFTPAgent()
try {
const resolvedFd = await downloadFileRaw(agent, fd, async (raw) => {
@@ -115,7 +115,7 @@ export function initDownload(app: HTMLElement, hash: string) {
else retryBtn.hidden = false
} finally {
await backend.cleanup().catch(() => {})
closeXFTPAgent(agent)
agent.close()
}
}
}
+4 -4
View File
@@ -2,7 +2,7 @@ import {createCryptoBackend} from './crypto-backend.js'
import {getServers} from './servers.js'
import {createProgressRing} from './progress.js'
import {
newXFTPAgent, closeXFTPAgent, uploadFile, encodeDescriptionURI,
XFTPAgent, uploadFile, encodeDescriptionURI,
XFTPPermanentError, type EncryptedFileMetadata
} from '../src/agent.js'
@@ -104,12 +104,12 @@ export function initUpload(app: HTMLElement) {
statusText.textContent = 'Encrypting…'
const backend = createCryptoBackend()
const agent = newXFTPAgent()
const agent = new XFTPAgent()
cancelBtn.onclick = () => {
aborted = true
backend.cleanup().catch(() => {})
closeXFTPAgent(agent)
agent.close()
showStage(dropZone)
}
@@ -157,7 +157,7 @@ export function initUpload(app: HTMLElement) {
}
} finally {
await backend.cleanup().catch(() => {})
closeXFTPAgent(agent)
agent.close()
}
}
}