mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-06-07 13:12:10 +00:00
convert XFTPClientAgent interface to XFTPAgent class
This commit is contained in:
+3
-3
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user