From 4df6a2c47fc48e7af7b6e7f45d7841ca4eb0d4a1 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 19 Feb 2026 07:50:15 +0000 Subject: [PATCH] convert XFTPClientAgent interface to XFTPAgent class --- xftp-web/README.md | 6 ++-- xftp-web/src/agent.ts | 26 +++++++------- xftp-web/src/client.ts | 52 +++++++++++++-------------- xftp-web/test/browser.test.ts | 6 ++-- xftp-web/test/connection.node.test.ts | 12 +++---- xftp-web/web/download.ts | 6 ++-- xftp-web/web/upload.ts | 8 ++--- 7 files changed, 57 insertions(+), 59 deletions(-) diff --git a/xftp-web/README.md b/xftp-web/README.md index 6e47f09d9..964ce1d53 100644 --- a/xftp-web/README.md +++ b/xftp-web/README.md @@ -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 diff --git a/xftp-web/src/agent.ts b/xftp-web/src/agent.ts index 139c67767..5ccfc6cb3 100644 --- a/xftp-web/src/agent.ts +++ b/xftp-web/src/agent.ts @@ -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 { @@ -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 export async function sendFile( - agent: XFTPClientAgent, servers: XFTPServer[], + agent: XFTPAgent, servers: XFTPServer[], source: AsyncIterable, sourceSize: number, fileName: string, options?: SendFileOptions ): Promise export async function sendFile( - agent: XFTPClientAgent, servers: XFTPServer[], + agent: XFTPAgent, servers: XFTPServer[], source: Uint8Array | AsyncIterable, 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, 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 { @@ -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 { @@ -504,7 +504,7 @@ export async function receiveFile( } async function resolveRedirect( - agent: XFTPClientAgent, + agent: XFTPAgent, fd: FileDescription ): Promise { 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 { +export async function deleteFile(agent: XFTPAgent, sndDescription: FileDescription): Promise { const byServer = new Map() for (const chunk of sndDescription.chunks) { const srv = chunk.replicas[0]?.server ?? "" diff --git a/xftp-web/src/client.ts b/xftp-web/src/client.ts index 3d1f823e5..587189cf1 100644 --- a/xftp-web/src/client.ts +++ b/xftp-web/src/client.ts @@ -172,17 +172,24 @@ interface ServerConnection { queue: Promise // tail of sequential command chain } -export interface XFTPClientAgent { - connections: Map +export class XFTPAgent { + connections = new Map() /** @internal Injectable for testing — defaults to connectXFTP */ _connectFn: (server: XFTPServer) => Promise + + constructor(connectFn?: (server: XFTPServer) => Promise) { + 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 { +export function getXFTPServerClient(agent: XFTPAgent, server: XFTPServer): Promise { 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 { +export function reconnectClient(agent: XFTPAgent, server: XFTPServer): Promise { 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 + agent: XFTPAgent, server: XFTPServer, failedP: Promise ): 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): Promise { @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { +export async function pingXFTP(agent: XFTPAgent, server: XFTPServer): Promise { const client = await getXFTPServerClient(agent, server) const corrId = new Uint8Array(0) const block = encodeTransmission(client.sessionId, corrId, new Uint8Array(0), encodePING()) diff --git a/xftp-web/test/browser.test.ts b/xftp-web/test/browser.test.ts index 26a9670ca..9c4c07aed 100644 --- a/xftp-web/test/browser.test.ts +++ b/xftp-web/test/browser.test.ts @@ -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() } }) diff --git a/xftp-web/test/connection.node.test.ts b/xftp-web/test/connection.node.test.ts index c04b7dd5a..0748790ed 100644 --- a/xftp-web/test/connection.node.test.ts +++ b/xftp-web/test/connection.node.test.ts @@ -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 { } } -function makeAgent(connectFn: (s: any) => Promise): XFTPClientAgent { - const agent = newXFTPAgent() - agent._connectFn = connectFn - return agent +function makeAgent(connectFn: (s: any) => Promise): 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) diff --git a/xftp-web/web/download.ts b/xftp-web/web/download.ts index 25443cf35..c3a1c494d 100644 --- a/xftp-web/web/download.ts +++ b/xftp-web/web/download.ts @@ -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() } } } diff --git a/xftp-web/web/upload.ts b/xftp-web/web/upload.ts index cf63ba379..ee39c569f 100644 --- a/xftp-web/web/upload.ts +++ b/xftp-web/web/upload.ts @@ -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() } } }